diff --git a/.ci.yaml b/.ci.yaml index 00aecdca1122..7504373ac61a 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -4,32 +4,24 @@ # for every commit. # # More information at: -# * https://github.com/flutter/cocoon/blob/master/CI_YAML.md +# * https://github.com/flutter/cocoon/blob/main/CI_YAML.md enabled_branches: - - master + - main 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 @@ -41,11 +33,12 @@ targets: properties: add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml + channel: master + version_file: flutter_master.version dependencies: > [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - scheduler: luci - name: Windows win32-platform_tests stable recipe: plugins/plugins @@ -56,9 +49,8 @@ targets: channel: stable dependencies: > [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - scheduler: luci - name: Windows windows-build_all_plugins master recipe: plugins/plugins @@ -66,11 +58,12 @@ targets: properties: add_recipes_cq: "true" target_file: build_all_plugins.yaml + channel: master + version_file: flutter_master.version dependencies: > [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - scheduler: luci - name: Windows windows-build_all_plugins stable recipe: plugins/plugins @@ -81,21 +74,8 @@ targets: channel: stable dependencies: > [ - {"dependency": "vs_build"} - ] - scheduler: luci - - - name: Windows uwp-platform_tests master - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: uwp_build_and_platform_tests.yaml - dependencies: > - [ - {"dependency": "vs_build"} + {"dependency": "vs_build", "version": "version:vs2019"} ] - scheduler: luci - name: Windows plugin_tools_tests recipe: plugins/plugins @@ -103,11 +83,11 @@ targets: properties: add_recipes_cq: "true" target_file: plugin_tools_tests.yaml - scheduler: luci + channel: master + version_file: flutter_master.version - name: Linux ci_yaml plugins roller recipe: infra/ci_yaml timeout: 30 - scheduler: luci runIf: - .ci.yaml diff --git a/.ci/Dockerfile b/.ci/Dockerfile index a3deb6948d90..59d4064f0a91 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,14 +1,10 @@ # 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.2.2 +FROM cirrusci/flutter@sha256:505fe8bce2896c75b4df9ccf500b1604155bf932af7465ffcc66fcae8612f82f RUN apt-get update -y -# Required by Roboeletric and the Android SDK. -RUN apt-get install -y openjdk-8-jdk -ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 - RUN apt-get install -y --no-install-recommends gnupg # Add repo for gcloud sdk and install it @@ -45,6 +41,7 @@ RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo ap 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: has a handler for url_launcher tests. +# Make Chrome the default so http: and file: has a handler for url_launcher tests. 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 new file mode 100644 index 000000000000..d8dd5baefc46 --- /dev/null +++ b/.ci/flutter_master.version @@ -0,0 +1 @@ +5a5d021e51ca9a16fdfc64b34a8d64c75be1b500 diff --git a/.ci/java8.Dockerfile b/.ci/java8.Dockerfile deleted file mode 100644 index fb1844a5401f..000000000000 --- a/.ci/java8.Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM cirrusci/flutter:stable - -RUN apt-get update -y - -# Required by Roboeletric and the Android SDK. -RUN apt-get install -y openjdk-8-jdk -ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 - -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" | \ - tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - -RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ - 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. -RUN apt-get install -y clang-format -# Required by Roboeletric and the Android SDK. -RUN apt-get install -y openjdk-8-jdk diff --git a/.ci/scripts/build_examples_uwp.sh b/.ci/scripts/build_examples_uwp.sh deleted file mode 100644 index 639cb054e4b7..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 diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh index 8c090f4b78d2..bcf57a4b311f 100644 --- a/.ci/scripts/build_examples_win32.sh +++ b/.ci/scripts/build_examples_win32.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ - --packages-for-branch + --packages-for-branch --log-timing diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh index 63abc06bec5a..c3e2e7bc5447 100644 --- a/.ci/scripts/drive_examples_win32.sh +++ b/.ci/scripts/drive_examples_win32.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ - --packages-for-branch + --exclude=script/configs/exclude_integration_win32.yaml --packages-for-branch --log-timing diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh index 938515784412..37cf54e55c5c 100644 --- a/.ci/scripts/native_test_win32.sh +++ b/.ci/scripts/native_test_win32.sh @@ -4,4 +4,4 @@ # found in the LICENSE file. dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ - --no-integration --packages-for-branch + --no-integration --packages-for-branch --log-timing diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh index 1095e2189a36..f93694bf1ff6 100644 --- 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 8dcb4d96d2be..6ea4680f6dec 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,29 +1,38 @@ -gcp_credentials: ENCRYPTED[!48cff44dd32e9cc412d4d381c7fe68d373ca04cf2639f8192d21cb1a9ab5e21129651423a1cf88f3fd7fe2125c1cabd9!] +gcp_credentials: ENCRYPTED[!ebad0a1f4f7a446b77944c33651460a7ab010b4617273cb016cf354eb8fc22aa92e37a3c58bfa4a0c40a799351e027a6!] -# 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" tool_setup_template: &TOOL_SETUP_TEMPLATE tool_setup_script: - - git fetch origin master # To set FETCH_HEAD for "git merge-base" to work + - git fetch origin main # To set FETCH_HEAD for "git merge-base" to work - cd script/tool - dart pub get 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. + - TARGET_TREEISH=$CHANNEL + - if [[ "$CHANNEL" == "master" ]]; then + - TARGET_TREEISH=$(< .ci/flutter_$CHANNEL.version) + - fi # Ensure that the repository has all the branches. - cd $FLUTTER_HOME - git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" - git fetch origin - # Switch to the requested branch. - - git checkout $CHANNEL - # Reset to upstream branch, rather than using pull, since the base image - # can sometimes be in a state where it has diverged from upstream (!). - - git reset --hard @{u} + # Switch to the requested channel. + - git checkout $TARGET_TREEISH + # 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. - flutter doctor -v << : *TOOL_SETUP_TEMPLATE @@ -46,8 +55,16 @@ 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-12.5 + image: big-sur-xcode-13 + +macos_arm_template: &MACOS_ARM_TEMPLATE + << : *MACOS_TEMPLATE + macos_instance: + image: ghcr.io/cirruslabs/macos-monterey-xcode:13.4 # Light-workload Linux tasks. # These use default machines, with fewer CPUs, to reduce pressure on the @@ -68,12 +85,35 @@ task: - cd script/tool - dart pub run test - name: publishable - version_check_script: ./script/tool_runner.sh version-check + env: + 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 - format_script: ./script/tool_runner.sh format --fail-on-change - pubspec_script: ./script/tool_runner.sh pubspec-check - license_script: dart $PLUGIN_TOOL license-check + always: + format_script: ./script/tool_runner.sh format --fail-on-change + pubspec_script: ./script/tool_runner.sh pubspec-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 + license_script: dart $PLUGIN_TOOL license-check + dependabot_script: dart $PLUGIN_TOOL dependabot-check - name: federated_safety # This check is only meaningful for PRs, as it validates changes # rather than state. @@ -91,21 +131,44 @@ task: matrix: CHANNEL: "master" CHANNEL: "stable" - tool_script: + analyze_tool_script: - cd script/tool - dart analyze --fatal-infos - script: + analyze_script: # DO NOT change the custom-analysis argument here without changing the Dart repo. # See the comment in script/configs/custom_analysis.yaml for details. - ./script/tool_runner.sh analyze --custom-analysis=script/configs/custom_analysis.yaml - ### Android tasks ### - - name: android-build_all_plugins + pathified_analyze_script: + # Re-run analysis with path-based dependencies to ensure that publishing + # the changes won't break analysis of other packages in the respository + # that depend on it. + - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates + # This uses --run-on-dirty-packages rather than --packages-for-branch + # since only the packages changed by 'make-deps-path-based' need to be + # checked. + - dart $PLUGIN_TOOL 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 plugins at least pass analysis on the N-1 and N-2 + # versions of Flutter stable if the plugin 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 env: - BUILD_ALL_ARGS: "apk" matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + CHANNEL: "2.10.5" + CHANNEL: "2.8.1" + analyze_script: + - ./script/tool_runner.sh analyze --skip-if-not-supporting-flutter-version="$CHANNEL" --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: @@ -125,6 +188,8 @@ task: - 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" @@ -154,17 +219,20 @@ 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 4" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" - PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" + 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" matrix: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!c9446a7b11d5520c2ebce3c64ccc82fe6d146272cb06a4a4590e22c389f33153f951347a25422522df1a81fe2f085e9a!] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[c84a06b85f9c906705732aea6142ef6f63ff1a6f07372dc36880a9d0c2c4b9cb35b2e35cd19edc6285167c2a5cc075ec] build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -197,7 +265,7 @@ task: - 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=flame,version=29 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml + - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi @@ -207,34 +275,42 @@ task: path: "**/reports/lint-results-debug.xml" type: text/xml format: android-lint + - name: android-build_all_plugins + env: + BUILD_ALL_ARGS: "apk" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: web-platform_tests env: + matrix: + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" matrix: CHANNEL: "master" CHANNEL: "stable" + CHROME_NO_SANDBOX: true + CHROME_DIR: /tmp/web_chromium/ + CHROME_EXECUTABLE: $CHROME_DIR/chrome-linux/chrome install_script: - - git clone https://github.com/flutter/web_installers.git - - cd web_installers/packages/web_drivers/ - - dart pub get + # Install a pinned version of Chromium and its corresponding ChromeDriver. + # Setting CHROME_EXECUTABLE above causes this version to be used for tests. + - ./script/install_chromium.sh "$CHROME_DIR" chromedriver_background_script: - - cd web_installers/packages/web_drivers/ - - dart lib/web_driver_installer.dart chromedriver --install-only + - cd "$CHROME_DIR" - ./chromedriver/chromedriver --port=4444 build_script: - ./script/tool_runner.sh build-examples --web 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: @@ -243,7 +319,47 @@ 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: + - flutter config --enable-macos-desktop + - ./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: @@ -257,19 +373,24 @@ task: SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] create_simulator_script: - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-5 | xargs xcrun simctl boot + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-15-0 | xargs xcrun simctl boot build_script: - ./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" --exclude=script/configs/exclude_native_ios.yaml + - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" drive_script: # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. # 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" @@ -279,18 +400,3 @@ task: 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 --exclude=script/configs/exclude_native_macos.yaml - drive_script: - - ./script/tool_runner.sh drive-examples --macos diff --git a/.clang-format b/.clang-format index f6cb8ad931f5..ac9a91a08008 100644 --- a/.clang-format +++ b/.clang-format @@ -1 +1,9 @@ BasedOnStyle: Google +--- +Language: Cpp +DerivePointerAlignment: false +PointerAlignment: Left +--- +Language: ObjC +DerivePointerAlignment: false +PointerAlignment: Right diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a3a279ab2151..9fe5a37a4fa8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,25 +8,28 @@ - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. -- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. (Note that unlike the flutter/flutter repo, the flutter/plugins repo does use `dart format`.) +- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. (Unlike the flutter/flutter repo, the flutter/plugins repo does use `dart format`.) - [ ] I signed the [CLA]. - [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. `[shared_preferences]` - [ ] I listed at least one issue that this PR fixes in the description above. -- [ ] I [updated pubspec.yaml](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates) with an appropriate new version according to the [pub versioning philosophy]. -- [ ] I updated CHANGELOG.md to add a description of the change. +- [ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. +- [ ] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style]. - [ ] I updated/added relevant documentation (doc comments with `///`). -- [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt. +- [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. -[Contributor Guide]: https://github.com/flutter/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 [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning -[the auto-formatter]: https://github.com/flutter/plugins/blob/master/script/tool/README.md#format-code +[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/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..33094a27e9a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,321 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/packages/camera/camera_android/android" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera_android/example/android/app" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/camera/camera/example/android/app" + commit-message: + prefix: "[camera]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/espresso/android" + commit-message: + prefix: "[espresso]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/espresso/example/android/app" + commit-message: + prefix: "[espresso]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/flutter_plugin_android_lifecycle/android" + commit-message: + prefix: "[lifecycle]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/flutter_plugin_android_lifecycle/example/android/app" + commit-message: + prefix: "[lifecycle]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/google_maps_flutter/google_maps_flutter/android" + commit-message: + prefix: "[google_maps]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - 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 + + - 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 + + - 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 + + - 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 + + - 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 + + - 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 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/image_picker/image_picker_android/android" + commit-message: + prefix: "[image_picker]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/local_auth/local_auth_android/android" + commit-message: + prefix: "[local_auth]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - 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 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/path_provider/path_provider_android/android" + commit-message: + prefix: "[path_provider]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/quick_actions/quick_actions_android/android" + commit-message: + prefix: "[quick_actions]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - 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 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/shared_preferences/shared_preferences_android/android" + commit-message: + prefix: "[shared_pref]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/url_launcher/url_launcher_android/android" + commit-message: + prefix: "[url_launcher]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - 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 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/video_player/video_player_android/android" + commit-message: + prefix: "[video_player]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter/example/android/app" + commit-message: + prefix: "[webview]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - package-ecosystem: "gradle" + directory: "/packages/webview_flutter/webview_flutter_android/android" + commit-message: + prefix: "[webview]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + - 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 + + - package-ecosystem: "github-actions" + directory: "/" + commit-message: + prefix: "[gh_actions]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/labeler.yml b/.github/labeler.yml index 38ee94c004f9..a87e83da3450 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,24 +1,6 @@ -'p: android_alarm_manager': - - packages/android_alarm_manager/**/* - -'p: android_intent': - - packages/android_intent/**/* - -'p: battery': - - packages/battery/**/* - 'p: camera': - packages/camera/**/* -'p: connectivity': - - packages/connectivity/**/* - -'p: cross_file': - - packages/cross_file/**/* - -'p: device_info': - - packages/device_info/**/* - 'p: espresso': - packages/espresso/**/* @@ -46,9 +28,6 @@ 'p: local_auth': - packages/local_auth/**/* -'p: package_info': - - packages/package_info/**/* - 'p: path_provider': - packages/path_provider/**/* @@ -58,12 +37,6 @@ 'p: quick_actions': - packages/quick_actions/**/* -'p: sensors': - - packages/sensors/**/* - -'p: share': - - packages/share/**/* - 'p: shared_preferences': - packages/shared_preferences/**/* @@ -76,15 +49,14 @@ 'p: webview_flutter': - packages/webview_flutter/**/* -'p: wifi_info_flutter': - - packages/wifi_info_flutter/**/* - 'platform-android': - packages/*/*_android/**/* - packages/**/android/**/* 'platform-ios': - packages/*/*_ios/**/* + - packages/*/*_storekit/**/* + - packages/*/*_wkwebview/**/* - packages/**/ios/**/* 'platform-linux': diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml index 825a3afd8508..dff3fbd411d6 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@9fd24f1f9d6ceb64ba34d181b329ee72f99978a0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a64acf7692f9..d4e2b11ec5f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,10 @@ name: release on: push: branches: - - master + - main + +# Declare default permissions as read only. +permissions: read-all jobs: release: @@ -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@2541b1294d2704b0964813337f33b291d3f8596b 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@752bfae19aef55dab12a00bc36d48acc46b77e9d with: ref: ${{ github.sha }} running-workflow-name: 'release' @@ -49,6 +52,3 @@ jobs: 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 env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} - - env: - DEFAULT_BRANCH: master diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml new file mode 100644 index 000000000000..7f2a4d8ec44e --- /dev/null +++ b/.github/workflows/scorecards-analysis.yml @@ -0,0 +1,53 @@ +name: Scorecards supply-chain security +on: + # Only the default branch is supported. + branch_protection_rule: + push: + branches: [ main ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + actions: read + contents: read + + steps: + - name: "Checkout code" + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@3e15ea8318eee9b333819ec77a36aca8d39df13e + with: + results_file: results.sarif + results_format: sarif + # Read-only PAT token. To create it, + # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. + repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + # Publish the results to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, + # regardless of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). + - name: "Upload artifact" + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@27ea8f8fe5977c00f5b37e076ab846c5bd783b96 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index f4fa0b9b7795..8eaeaff8de55 100644 --- a/.gitignore +++ b/.gitignore @@ -34,11 +34,12 @@ gradle-wrapper.jar .flutter-plugins-dependencies *.iml +generated_plugin_registrant.cc +generated_plugin_registrant.h generated_plugin_registrant.dart +GeneratedPluginRegistrant.java GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m -generated_plugin_registrant.cc -GeneratedPluginRegistrant.java GeneratedPluginRegistrant.swift build/ .flutter-plugins 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 0ca697b6a756..31402c79d54a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -65,3 +65,6 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Daniel Roek +TheOneWithTheBraid +Rulong Chen(陈汝龙) +Hwanseok Kang diff --git a/CODEOWNERS b/CODEOWNERS index 1d52dcefcbef..16bf98ba9e4b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,10 +4,72 @@ # These names are just suggestions. It is fine to have your changes # reviewed by someone else. +# Plugin-level rules. +packages/camera/** @bparrishMines +packages/file_selector/** @stuartmorgan +packages/google_maps_flutter/** @stuartmorgan +packages/google_sign_in/** @stuartmorgan +packages/image_picker/** @stuartmorgan +packages/local_auth/** @stuartmorgan +packages/path_provider/** @gaaclarke +packages/plugin_platform_interface/** @stuartmorgan +packages/quick_actions/** @stuartmorgan +packages/shared_preferences/** @gaaclarke +packages/url_launcher/** @stuartmorgan +packages/video_player/** @gaaclarke +packages/webview_flutter/** @bparrishMines -packages/camera/** @bparrishMines -packages/file_selector/** @ditman -packages/google_maps_flutter/** @cyanglaz -packages/image_picker/** @cyanglaz -packages/in_app_purchase/** @cyanglaz @LHLL -packages/ios_platform_images/** @gaaclarke +# Sub-package-level rules. These should stay last, since the last matching +# entry takes precedence. + +# - Web +packages/**/*_web/** @ditman + +# - Android +packages/camera/camera_android/** @camsim99 +packages/espresso/** @blasten +packages/flutter_plugin_android_lifecycle/** @blasten +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/** @blasten +packages/local_auth/local_auth_android/** @blasten +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/** @blasten + +# - iOS +packages/camera/camera_avfoundation/** @hellohuanlin +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/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 ac66886c1ff9..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)_ @@ -18,21 +18,12 @@ Additional resources specific to the plugins repository: - [Plugin tests](https://github.com/flutter/flutter/wiki/Plugin-Tests), which explains the different kinds of tests used for plugins, where to find them, and how to run them. As explained in the Flutter guide, - [**PRs needs tests**](https://github.com/flutter/flutter/wiki/Tree-hygiene#tests), so + [**PRs need tests**](https://github.com/flutter/flutter/wiki/Tree-hygiene#tests), so this is critical to read before submitting a PR. - [Contributing to Plugins and Packages](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages), for more information about how to make PRs for this repository, especially when changing federated plugins. -## Important note - -As of January 2021, we are no longer accepting non-critical PRs for the -[deprecated plugins](./README.md#deprecated), as all new development should -happen in the Flutter Community Plus replacements. If you have a PR for -something other than a critical issue (crashes, build failures, security issues) -in one of those pluigns, please [submit it to the Flutter Community Plus -replacement](https://github.com/fluttercommunity/plus_plugins/pulls) instead. - ## Other notes ### Style @@ -44,35 +35,13 @@ 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` - [Objective-C](https://google.github.io/styleguide/objcguide.html) formatted with `clang-format` -### The review process - -Reviewing PRs often requires a non-trivial amount of time. We prioritize issues, not PRs, so that we use our maintainers' time in the most impactful way. Issues pertaining to this repository are managed in the [flutter/flutter issue tracker and are labeled with "plugin"](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin+sort%3Areactions-%2B1-desc). Non-trivial PRs should have an associated issue that will be used for prioritization. See the [prioritization section](https://github.com/flutter/flutter/wiki/Issue-hygiene#prioritization) in the Flutter wiki to understand how issues are prioritized. - -Newly opened PRs first go through initial triage which results in one of: - * **Merging the PR** - if the PR can be quickly reviewed and looks good. - * **Requesting minor changes** - if the PR can be quickly reviewed, but needs changes. - * **Moving the PR to the backlog** - if the review requires non-trivial effort and the issue isn't currently a priority; in this case the maintainer will: - * Add the "backlog" label to the issue. - * Leave a comment on the PR explaining that the review is not trivial and that the issue will be looked at according to priority order. - * **Starting a non-trivial review** - if the review requires non-trivial effort and the issue is a priority; in this case the maintainer will: - * Add the "in review" label to the issue. - * Self assign the PR. - * **Closing the PR** - if the PR maintainer decides that the PR should not be merged. - -Please be aware that there is currently a significant backlog, so reviews for plugin PRs will -in most cases take significantly longer to begin than the two-week timeframe given in the -main Flutter PR guide. An effort is underway to work through the backlog, but it will -take time. If you are interested in hepling out (e.g., by doing initial reviews looking -for obvious problems like missing or failing tests), please reach out -[on Discord](https://github.com/flutter/flutter/wiki/Chat) in `#hackers-ecosystem`. - ### Releasing If you are a team member landing a PR, or just want to know what the release diff --git a/README.md b/README.md index 2d8f4f502e17..edcffe208cad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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) This repo is a companion repo to the main [flutter @@ -30,48 +30,32 @@ see the documentation for [developing packages](https://flutter.dev/developing-p [platform channels](https://flutter.dev/platform-channels/). You can store your plugin source code in any GitHub repository (the present repo is only intended for plugins developed by the core Flutter team). Once your plugin -is ready you can [publish](https://flutter.dev/developing-packages/#publish) +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) | -| [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) | - -### Deprecated - -The following plugins are also part of this repository, but are deprecated in -favor of the [Flutter Community Plus](https://plus.fluttercommunity.dev/) versions. - -| Plugin | Pub | | Replacement | Pub | -|--------|-----|--|-------------|-----| -| [android_alarm_manager](./packages/android_alarm_manager/) | [![pub package](https://img.shields.io/pub/v/android_alarm_manager.svg)](https://pub.dev/packages/android_alarm_manager) | | android_alarm_manager_plus | [![pub package](https://img.shields.io/pub/v/android_alarm_manager_plus.svg)](https://pub.dev/packages/android_alarm_manager_plus) | -| [android_intent](./packages/android_intent/) | [![pub package](https://img.shields.io/pub/v/android_intent.svg)](https://pub.dev/packages/android_intent) | | android_intent_plus | [![pub package](https://img.shields.io/pub/v/android_intent_plus.svg)](https://pub.dev/packages/android_intent_plus) | -| [battery](./packages/battery/) | [![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dev/packages/battery) | | battery_plus | [![pub package](https://img.shields.io/pub/v/battery_plus.svg)](https://pub.dev/packages/battery_plus) | -| [connectivity](./packages/connectivity/) | [![pub package](https://img.shields.io/pub/v/connectivity.svg)](https://pub.dev/packages/connectivity) | | connectivity_plus | [![pub package](https://img.shields.io/pub/v/connectivity_plus.svg)](https://pub.dev/packages/connectivity_plus) | -| [device_info](./packages/device_info/) | [![pub package](https://img.shields.io/pub/v/device_info.svg)](https://pub.dev/packages/device_info) | | device_info_plus | [![pub package](https://img.shields.io/pub/v/device_info_plus.svg)](https://pub.dev/packages/device_info_plus) | -| [package_info](./packages/package_info/) | [![pub package](https://img.shields.io/pub/v/package_info.svg)](https://pub.dev/packages/package_info) | | package_info_plus | [![pub package](https://img.shields.io/pub/v/package_info_plus.svg)](https://pub.dev/packages/package_info_plus) | -| [sensors](./packages/sensors/) | [![pub package](https://img.shields.io/pub/v/sensors.svg)](https://pub.dev/packages/sensors) | | sensors_plus | [![pub package](https://img.shields.io/pub/v/sensors_plus.svg)](https://pub.dev/packages/sensors_plus) | -| [share](./packages/share/) | [![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dev/packages/share) | | share_plus | [![pub package](https://img.shields.io/pub/v/share_plus.svg)](https://pub.dev/packages/share_plus) | -| [wifi_info_flutter](./packages/wifi_info_flutter/) | [![pub package](https://img.shields.io/pub/v/wifi_info_flutter.svg)](https://pub.dev/packages/wifi_info_flutter) | | network_info_plus | [![pub package](https://img.shields.io/pub/v/network_info_plus.svg)](https://pub.dev/packages/network_info_plus) | +| 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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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://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) | [![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 901067736edc..87515a471050 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,30 +2,24 @@ # 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: @@ -36,7 +30,7 @@ analyzer: 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,23 +39,21 @@ 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' - 'lib/src/generated/*.dart' - '**/*.mocks.dart' # Mockito @GenerateMocks + - '**/*.pigeon.dart' # Pigeon generated file 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 @@ -71,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 # LOCAL CHANGE - Needs to be enabled and violations fixed. - 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 # LOCAL CHANGE - Needs to be enabled and violations fixed. + - 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 @@ -136,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 # LOCAL CHANGE - Needs to be enabled and violations fixed. - 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 @@ -173,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 # LOCAL CHANGE - Needs to be enabled and violations fixed. - 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 # LOCAL CHANGE - Needs to be enabled and violations fixed. - 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 793640e22d27..000000000000 --- a/analysis_options_legacy.yaml +++ /dev/null @@ -1,16 +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 - 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/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md deleted file mode 100644 index d53b932e3f0f..000000000000 --- a/packages/android_alarm_manager/CHANGELOG.md +++ /dev/null @@ -1,277 +0,0 @@ -## NEXT - -* Remove support for the V1 Android embedding. -* Updated Android lint settings. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 0.4.5+20 - -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. - -## 0.4.5+19 - -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) - -## 0.4.5+18 - -* Update Flutter SDK constraint. - -## 0.4.5+17 - -* Update Dart SDK constraint in example. - -## 0.4.5+16 - -* Remove unnecessary workaround from test. - -## 0.4.5+15 - -* Update android compileSdkVersion to 29. - -## 0.4.5+14 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.5+13 - -* Android Code Inspection and Clean up. - -## 0.4.5+12 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.5+11 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.5+10 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.5+9 - -* Fix CocoaPods podspec lint warnings. - -## 0.4.5+8 - -* Remove `MainActivity` references in android example app and tests. - -## 0.4.5+7 - -* Update minimum Flutter version to 1.12.13+hotfix.5 -* Clean up various Android workarounds no longer needed after framework v1.12. -* Complete v2 embedding support. - -## 0.4.5+6 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.5+5 - -* Added an Espresso test. - -## 0.4.5+4 - -* Make the pedantic dev_dependency explicit. - -## 0.4.5+3 - -* Fixed issue where callback lookup would fail while running in the background. - -## 0.4.5+2 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.5+1 - -* Loosen Flutter version restriction to 1.9.1. **NOTE: plugin registration - for the background isolate will not work correctly for applications using the - V2 Flutter Android embedding for Flutter versions lower than 1.12.** - -## 0.4.5 - -* Add support for Flutter Android embedding V2 - -## 0.4.4+3 - -* Add unit tests and DartDocs. - -## 0.4.4+2 - -* Remove AndroidX warning. - -## 0.4.4+1 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.4.4 - -* Add `id` to `callback` if it is of type `Function(int)` - -## 0.4.3 - -* Added `oneShotAt` method to run `callback` at a given DateTime `time`. - -## 0.4.2 - -* Added support for setting alarms which work when the phone is in doze mode. - -## 0.4.1+8 - -* Remove dependency on google-services in the Android example. - -## 0.4.1+7 - -* Fix possible crash on Android devices with APIs below 19. - -## 0.4.1+6 - -* Bump the minimum Flutter version to 1.2.0. -* Add template type parameter to `invokeMethod` calls. - -## 0.4.1+5 - -* Update AlarmService to throw a `PluginRegistrantException` if - `AlarmService.setPluginRegistrant` has not been called to set a - PluginRegistrantCallback. This improves the error message seen when the - `AlarmService.setPluginRegistrant` call is omitted. - -## 0.4.1+4 - -* Updated example to remove dependency on Firebase. - -## 0.4.1+3 - -* Update README.md to include instructions for setting the WAKE_LOCK permission. -* Updated example application to use the WAKE_LOCK permission. - -## 0.4.1+2 - -* Include a missing API dependency. - -## 0.4.1+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.1 -* Added support for setting alarms which persist across reboots. - * Both `AndroidAlarmManager.oneShot` and `AndroidAlarmManager.periodic` have - an optional `rescheduleOnReboot` parameter which specifies whether the new - alarm should be rescheduled to run after a reboot (default: false). If set - to false, the alarm will not survive a device reboot. - * Requires AndroidManifest.xml to be updated to include the following - entries: - - ```xml - - - - - - - - - - - ``` - -## 0.4.0 - -* **Breaking change**. Migrated the underlying AlarmService to utilize a - BroadcastReceiver with a JobIntentService instead of a Service to handle - processing of alarms. This requires AndroidManifest.xml to be updated to - include the following entries: - - ```xml - - - ``` - -* Fixed issue where background service was not starting due to background - execution restrictions on Android 8+ (see [issue - #26846](https://github.com/flutter/flutter/issues/26846)). -* Fixed issue where alarm events were ignored when the background isolate was - still initializing. Alarm events are now queued if the background isolate has - not completed initializing and are processed once initialization is complete. - -## 0.3.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.2.3 -* Move firebase_auth from a dependency to a dev_dependency. - -## 0.2.2 -* Update dependencies for example to point to published versions of firebase_auth. - -## 0.2.1 -* Update dependencies for example to point to published versions of firebase_auth - and google_sign_in. -* Add missing dependency on firebase_auth. - -## 0.2.0 - -* **Breaking change**. A new isolate is always spawned for the background service - instead of trying to share an existing isolate owned by the application. -* **Breaking change**. Removed `AlarmService.getSharedFlutterView`. - -## 0.1.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.1.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.0.5 - -* Simplified and upgraded Android project template to Android SDK 27. -* Moved Android package to io.flutter.plugins. - -## 0.0.4 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.3 - -* Adds use of a Firebase plugin to the example. The example also now - demonstrates overriding the Application's onCreate method so that the - AlarmService can initialize plugin connections. - -## 0.0.2 - -* Add FLT prefix to iOS types. - -## 0.0.1 - -* Initial release. diff --git a/packages/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md deleted file mode 100644 index beefa985ef10..000000000000 --- a/packages/android_alarm_manager/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# android_alarm_manager - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`android_alarm_manager_plus`](https://pub.dev/packages/android_alarm_manager_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -[![pub package](https://img.shields.io/pub/v/android_alarm_manager.svg)](https://pub.dev/packages/android_alarm_manager) - -A Flutter plugin for accessing the Android AlarmManager service, and running -Dart code in the background when alarms fire. - -## Getting Started - -After importing this plugin to your project as usual, add the following to your -`AndroidManifest.xml` within the `` tags: - -```xml - - -``` - -Next, within the `` tags, add: - -```xml - - - - - - - - -``` - -Then in Dart code add: - -```dart -import 'package:android_alarm_manager/android_alarm_manager.dart'; - -void printHello() { - final DateTime now = DateTime.now(); - final int isolateId = Isolate.current.hashCode; - print("[$now] Hello, world! isolate=${isolateId} function='$printHello'"); -} - -main() async { - final int helloAlarmID = 0; - await AndroidAlarmManager.initialize(); - runApp(...); - await AndroidAlarmManager.periodic(const Duration(minutes: 1), helloAlarmID, printHello); -} -``` - -`printHello` will then run (roughly) every minute, even if the main app ends. However, `printHello` -will not run in the same isolate as the main application. Unlike threads, isolates do not share -memory and communication between isolates must be done via message passing (see more documentation on -isolates [here](https://api.dart.dev/stable/2.0.0/dart-isolate/dart-isolate-library.html)). - -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/android_alarm_manager/analysis_options.yaml b/packages/android_alarm_manager/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/android_alarm_manager/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle deleted file mode 100644 index 7712ed56fe6f..000000000000 --- a/packages/android_alarm_manager/android/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -group 'io.flutter.plugins.androidalarmmanager' -version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - baseline file("lint-baseline.xml") - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - implementation 'androidx.appcompat:appcompat:1.0.0' - api 'androidx.core:core:1.0.1' -} diff --git a/packages/android_alarm_manager/android/lint-baseline.xml b/packages/android_alarm_manager/android/lint-baseline.xml deleted file mode 100644 index de588614fdb2..000000000000 --- a/packages/android_alarm_manager/android/lint-baseline.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/android/settings.gradle b/packages/android_alarm_manager/android/settings.gradle deleted file mode 100644 index b0d09a021a46..000000000000 --- a/packages/android_alarm_manager/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'android_alarm_manager' diff --git a/packages/android_alarm_manager/android/src/main/AndroidManifest.xml b/packages/android_alarm_manager/android/src/main/AndroidManifest.xml deleted file mode 100644 index de6d16c038df..000000000000 --- a/packages/android_alarm_manager/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmBroadcastReceiver.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmBroadcastReceiver.java deleted file mode 100644 index c471643628bc..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmBroadcastReceiver.java +++ /dev/null @@ -1,31 +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.androidalarmmanager; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -public class AlarmBroadcastReceiver extends BroadcastReceiver { - /** - * Invoked by the OS when a timer goes off. - * - *

The associated timer was registered in {@link AlarmService}. - * - *

In Android, timer notifications require a {@link BroadcastReceiver} as the artifact that is - * notified when the timer goes off. As a result, this method is kept simple, immediately - * offloading any work to {@link AlarmService#enqueueAlarmProcessing(Context, Intent)}. - * - *

This method is the beginning of an execution path that will eventually execute a desired - * Dart callback function, as registed by the Dart side of the android_alarm_manager plugin. - * However, there may be asynchronous gaps between {@code onReceive()} and the eventual invocation - * of the Dart callback because {@link AlarmService} may need to spin up a Flutter execution - * context before the callback can be invoked. - */ - @Override - public void onReceive(Context context, Intent intent) { - AlarmService.enqueueAlarmProcessing(context, intent); - } -} diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java deleted file mode 100644 index aa59b578b157..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java +++ /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. - -package io.flutter.plugins.androidalarmmanager; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Handler; -import android.util.Log; -import androidx.core.app.AlarmManagerCompat; -import androidx.core.app.JobIntentService; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import org.json.JSONException; -import org.json.JSONObject; - -public class AlarmService extends JobIntentService { - private static final String TAG = "AlarmService"; - private static final String PERSISTENT_ALARMS_SET_KEY = "persistent_alarm_ids"; - protected static final String SHARED_PREFERENCES_KEY = "io.flutter.android_alarm_manager_plugin"; - private static final int JOB_ID = 1984; // Random job ID. - private static final Object persistentAlarmsLock = new Object(); - - // TODO(mattcarroll): make alarmQueue per-instance, not static. - private static List alarmQueue = Collections.synchronizedList(new LinkedList()); - - /** Background Dart execution context. */ - private static FlutterBackgroundExecutor flutterBackgroundExecutor; - - /** Schedule the alarm to be handled by the {@link AlarmService}. */ - public static void enqueueAlarmProcessing(Context context, Intent alarmContext) { - enqueueWork(context, AlarmService.class, JOB_ID, alarmContext); - } - - /** - * Starts the background isolate for the {@link AlarmService}. - * - *

Preconditions: - * - *

    - *
  • The given {@code callbackHandle} must correspond to a registered Dart callback. If the - * handle does not resolve to a Dart callback then this method does nothing. - *
- */ - public static void startBackgroundIsolate(Context context, long callbackHandle) { - if (flutterBackgroundExecutor != null) { - Log.w(TAG, "Attempted to start a duplicate background isolate. Returning..."); - return; - } - flutterBackgroundExecutor = new FlutterBackgroundExecutor(); - flutterBackgroundExecutor.startBackgroundIsolate(context, callbackHandle); - } - - /** - * Called once the Dart isolate ({@code flutterBackgroundExecutor}) has finished initializing. - * - *

Invoked by {@link AndroidAlarmManagerPlugin} when it receives the {@code - * AlarmService.initialized} message. Processes all alarm events that came in while the isolate - * was starting. - */ - /* package */ static void onInitialized() { - Log.i(TAG, "AlarmService started!"); - synchronized (alarmQueue) { - // Handle all the alarm events received before the Dart isolate was - // initialized, then clear the queue. - for (Intent intent : alarmQueue) { - flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(intent, null); - } - alarmQueue.clear(); - } - } - - /** - * Sets the Dart callback handle for the Dart method that is responsible for initializing the - * background Dart isolate, preparing it to receive Dart callback tasks requests. - */ - public static void setCallbackDispatcher(Context context, long callbackHandle) { - FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle); - } - - private static void scheduleAlarm( - Context context, - int requestCode, - boolean alarmClock, - boolean allowWhileIdle, - boolean repeating, - boolean exact, - boolean wakeup, - long startMillis, - long intervalMillis, - boolean rescheduleOnReboot, - long callbackHandle) { - if (rescheduleOnReboot) { - addPersistentAlarm( - context, - requestCode, - alarmClock, - allowWhileIdle, - repeating, - exact, - wakeup, - startMillis, - intervalMillis, - callbackHandle); - } - - // Create an Intent for the alarm and set the desired Dart callback handle. - Intent alarm = new Intent(context, AlarmBroadcastReceiver.class); - alarm.putExtra("id", requestCode); - alarm.putExtra("callbackHandle", callbackHandle); - PendingIntent pendingIntent = - PendingIntent.getBroadcast(context, requestCode, alarm, PendingIntent.FLAG_UPDATE_CURRENT); - - // Use the appropriate clock. - int clock = AlarmManager.RTC; - if (wakeup) { - clock = AlarmManager.RTC_WAKEUP; - } - - // Schedule the alarm. - AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - - if (alarmClock) { - AlarmManagerCompat.setAlarmClock(manager, startMillis, pendingIntent, pendingIntent); - return; - } - - if (exact) { - if (repeating) { - manager.setRepeating(clock, startMillis, intervalMillis, pendingIntent); - } else { - if (allowWhileIdle) { - AlarmManagerCompat.setExactAndAllowWhileIdle(manager, clock, startMillis, pendingIntent); - } else { - AlarmManagerCompat.setExact(manager, clock, startMillis, pendingIntent); - } - } - } else { - if (repeating) { - manager.setInexactRepeating(clock, startMillis, intervalMillis, pendingIntent); - } else { - if (allowWhileIdle) { - AlarmManagerCompat.setAndAllowWhileIdle(manager, clock, startMillis, pendingIntent); - } else { - manager.set(clock, startMillis, pendingIntent); - } - } - } - } - - /** Schedules a one-shot alarm to be executed once in the future. */ - public static void setOneShot(Context context, AndroidAlarmManagerPlugin.OneShotRequest request) { - final boolean repeating = false; - scheduleAlarm( - context, - request.requestCode, - request.alarmClock, - request.allowWhileIdle, - repeating, - request.exact, - request.wakeup, - request.startMillis, - 0, - request.rescheduleOnReboot, - request.callbackHandle); - } - - /** Schedules a periodic alarm to be executed repeatedly in the future. */ - public static void setPeriodic( - Context context, AndroidAlarmManagerPlugin.PeriodicRequest request) { - final boolean repeating = true; - final boolean allowWhileIdle = false; - final boolean alarmClock = false; - scheduleAlarm( - context, - request.requestCode, - alarmClock, - allowWhileIdle, - repeating, - request.exact, - request.wakeup, - request.startMillis, - request.intervalMillis, - request.rescheduleOnReboot, - request.callbackHandle); - } - - /** Cancels an alarm with ID {@code requestCode}. */ - public static void cancel(Context context, int requestCode) { - // Clear the alarm if it was set to be rescheduled after reboots. - clearPersistentAlarm(context, requestCode); - - // Cancel the alarm with the system alarm service. - Intent alarm = new Intent(context, AlarmBroadcastReceiver.class); - PendingIntent existingIntent = - PendingIntent.getBroadcast(context, requestCode, alarm, PendingIntent.FLAG_NO_CREATE); - if (existingIntent == null) { - Log.i(TAG, "cancel: broadcast receiver not found"); - return; - } - AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - manager.cancel(existingIntent); - } - - private static String getPersistentAlarmKey(int requestCode) { - return "android_alarm_manager/persistent_alarm_" + requestCode; - } - - private static void addPersistentAlarm( - Context context, - int requestCode, - boolean alarmClock, - boolean allowWhileIdle, - boolean repeating, - boolean exact, - boolean wakeup, - long startMillis, - long intervalMillis, - long callbackHandle) { - HashMap alarmSettings = new HashMap<>(); - alarmSettings.put("alarmClock", alarmClock); - alarmSettings.put("allowWhileIdle", allowWhileIdle); - alarmSettings.put("repeating", repeating); - alarmSettings.put("exact", exact); - alarmSettings.put("wakeup", wakeup); - alarmSettings.put("startMillis", startMillis); - alarmSettings.put("intervalMillis", intervalMillis); - alarmSettings.put("callbackHandle", callbackHandle); - JSONObject obj = new JSONObject(alarmSettings); - String key = getPersistentAlarmKey(requestCode); - SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); - - synchronized (persistentAlarmsLock) { - Set persistentAlarms = prefs.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); - if (persistentAlarms == null) { - persistentAlarms = new HashSet<>(); - } - if (persistentAlarms.isEmpty()) { - RebootBroadcastReceiver.enableRescheduleOnReboot(context); - } - persistentAlarms.add(Integer.toString(requestCode)); - prefs - .edit() - .putString(key, obj.toString()) - .putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms) - .apply(); - } - } - - private static void clearPersistentAlarm(Context context, int requestCode) { - String request = String.valueOf(requestCode); - SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); - synchronized (persistentAlarmsLock) { - Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); - if ((persistentAlarms == null) || !persistentAlarms.contains(request)) { - return; - } - persistentAlarms.remove(request); - String key = getPersistentAlarmKey(requestCode); - p.edit().remove(key).putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms).apply(); - - if (persistentAlarms.isEmpty()) { - RebootBroadcastReceiver.disableRescheduleOnReboot(context); - } - } - } - - public static void reschedulePersistentAlarms(Context context) { - synchronized (persistentAlarmsLock) { - SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); - Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); - // No alarms to reschedule. - if (persistentAlarms == null) { - return; - } - - for (String persistentAlarm : persistentAlarms) { - int requestCode = Integer.parseInt(persistentAlarm); - String key = getPersistentAlarmKey(requestCode); - String json = p.getString(key, null); - if (json == null) { - Log.e(TAG, "Data for alarm request code " + requestCode + " is invalid."); - continue; - } - try { - JSONObject alarm = new JSONObject(json); - boolean alarmClock = alarm.getBoolean("alarmClock"); - boolean allowWhileIdle = alarm.getBoolean("allowWhileIdle"); - boolean repeating = alarm.getBoolean("repeating"); - boolean exact = alarm.getBoolean("exact"); - boolean wakeup = alarm.getBoolean("wakeup"); - long startMillis = alarm.getLong("startMillis"); - long intervalMillis = alarm.getLong("intervalMillis"); - long callbackHandle = alarm.getLong("callbackHandle"); - scheduleAlarm( - context, - requestCode, - alarmClock, - allowWhileIdle, - repeating, - exact, - wakeup, - startMillis, - intervalMillis, - false, - callbackHandle); - } catch (JSONException e) { - Log.e(TAG, "Data for alarm request code " + requestCode + " is invalid: " + json); - } - } - } - } - - @Override - public void onCreate() { - super.onCreate(); - if (flutterBackgroundExecutor == null) { - flutterBackgroundExecutor = new FlutterBackgroundExecutor(); - } - Context context = getApplicationContext(); - flutterBackgroundExecutor.startBackgroundIsolate(context); - } - - /** - * Executes a Dart callback, as specified within the incoming {@code intent}. - * - *

Invoked by our {@link JobIntentService} superclass after a call to {@link - * JobIntentService#enqueueWork(Context, Class, int, Intent);}. - * - *

If there are no pre-existing callback execution requests, other than the incoming {@code - * intent}, then the desired Dart callback is invoked immediately. - * - *

If there are any pre-existing callback requests that have yet to be executed, the incoming - * {@code intent} is added to the {@link #alarmQueue} to invoked later, after all pre-existing - * callbacks have been executed. - */ - @Override - protected void onHandleWork(final Intent intent) { - // If we're in the middle of processing queued alarms, add the incoming - // intent to the queue and return. - synchronized (alarmQueue) { - if (!flutterBackgroundExecutor.isRunning()) { - Log.i(TAG, "AlarmService has not yet started."); - alarmQueue.add(intent); - return; - } - } - - // There were no pre-existing callback requests. Execute the callback - // specified by the incoming intent. - final CountDownLatch latch = new CountDownLatch(1); - new Handler(getMainLooper()) - .post( - new Runnable() { - @Override - public void run() { - flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(intent, latch); - } - }); - - try { - latch.await(); - } catch (InterruptedException ex) { - Log.i(TAG, "Exception waiting to execute Dart callback", ex); - } - } -} diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java deleted file mode 100644 index 45f047b5ae68..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AndroidAlarmManagerPlugin.java +++ /dev/null @@ -1,251 +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.androidalarmmanager; - -import android.content.Context; -import android.util.Log; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.JSONMethodCodec; -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 org.json.JSONArray; -import org.json.JSONException; - -/** - * Flutter plugin for running one-shot and periodic tasks sometime in the future on Android. - * - *

Plugin initialization goes through these steps: - * - *

    - *
  1. Flutter app instructs this plugin to initialize() on the Dart side. - *
  2. The Dart side of this plugin sends the Android side a "AlarmService.start" message, along - * with a Dart callback handle for a Dart callback that should be immediately invoked by a - * background Dart isolate. - *
  3. The Android side of this plugin spins up a background {@link FlutterEngine}, which includes - * a background Dart isolate. - *
  4. The Android side of this plugin instructs the new background Dart isolate to execute the - * callback that was received in the "AlarmService.start" message. - *
  5. The Dart side of this plugin, running within the new background isolate, executes the - * designated callback. This callback prepares the background isolate to then execute any - * given Dart callback from that point forward. Thus, at this moment the plugin is fully - * initialized and ready to execute arbitrary Dart tasks in the background. The Dart side of - * this plugin sends the Android side a "AlarmService.initialized" message to signify that the - * Dart is ready to execute tasks. - *
- */ -public class AndroidAlarmManagerPlugin implements FlutterPlugin, MethodCallHandler { - private static AndroidAlarmManagerPlugin instance; - private final String TAG = "AndroidAlarmManagerPlugin"; - private Context context; - private Object initializationLock = new Object(); - private MethodChannel alarmManagerPluginChannel; - - /** - * Registers this plugin with an associated Flutter execution context, represented by the given - * {@link io.flutter.plugin.common.PluginRegistry.Registrar}. - * - *

Once this method is executed, an instance of {@code AndroidAlarmManagerPlugin} will be - * connected to, and running against, the associated Flutter execution context. - */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - if (instance == null) { - instance = new AndroidAlarmManagerPlugin(); - } - instance.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - public void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { - synchronized (initializationLock) { - if (alarmManagerPluginChannel != null) { - return; - } - - Log.i(TAG, "onAttachedToEngine"); - this.context = applicationContext; - - // alarmManagerPluginChannel is the channel responsible for receiving the following messages - // from the main Flutter app: - // - "AlarmService.start" - // - "Alarm.oneShotAt" - // - "Alarm.periodic" - // - "Alarm.cancel" - alarmManagerPluginChannel = - new MethodChannel( - messenger, "plugins.flutter.io/android_alarm_manager", JSONMethodCodec.INSTANCE); - - // Instantiate a new AndroidAlarmManagerPlugin and connect the primary method channel for - // Android/Flutter communication. - alarmManagerPluginChannel.setMethodCallHandler(this); - } - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - Log.i(TAG, "onDetachedFromEngine"); - context = null; - alarmManagerPluginChannel.setMethodCallHandler(null); - alarmManagerPluginChannel = null; - } - - public AndroidAlarmManagerPlugin() {} - - /** Invoked when the Flutter side of this plugin sends a message to the Android side. */ - @Override - public void onMethodCall(MethodCall call, Result result) { - String method = call.method; - Object arguments = call.arguments; - try { - switch (method) { - case "AlarmService.start": - // This message is sent when the Dart side of this plugin is told to initialize. - long callbackHandle = ((JSONArray) arguments).getLong(0); - // In response, this (native) side of the plugin needs to spin up a background - // Dart isolate by using the given callbackHandle, and then setup a background - // method channel to communicate with the new background isolate. Once completed, - // this onMethodCall() method will receive messages from both the primary and background - // method channels. - AlarmService.setCallbackDispatcher(context, callbackHandle); - AlarmService.startBackgroundIsolate(context, callbackHandle); - result.success(true); - break; - case "Alarm.periodic": - // This message indicates that the Flutter app would like to schedule a periodic - // task. - PeriodicRequest periodicRequest = PeriodicRequest.fromJson((JSONArray) arguments); - AlarmService.setPeriodic(context, periodicRequest); - result.success(true); - break; - case "Alarm.oneShotAt": - // This message indicates that the Flutter app would like to schedule a one-time - // task. - OneShotRequest oneShotRequest = OneShotRequest.fromJson((JSONArray) arguments); - AlarmService.setOneShot(context, oneShotRequest); - result.success(true); - break; - case "Alarm.cancel": - // This message indicates that the Flutter app would like to cancel a previously - // scheduled task. - int requestCode = ((JSONArray) arguments).getInt(0); - AlarmService.cancel(context, requestCode); - result.success(true); - break; - default: - result.notImplemented(); - break; - } - } catch (JSONException e) { - result.error("error", "JSON error: " + e.getMessage(), null); - } - } - - /** A request to schedule a one-shot Dart task. */ - static final class OneShotRequest { - static OneShotRequest fromJson(JSONArray json) throws JSONException { - int requestCode = json.getInt(0); - boolean alarmClock = json.getBoolean(1); - boolean allowWhileIdle = json.getBoolean(2); - boolean exact = json.getBoolean(3); - boolean wakeup = json.getBoolean(4); - long startMillis = json.getLong(5); - boolean rescheduleOnReboot = json.getBoolean(6); - long callbackHandle = json.getLong(7); - - return new OneShotRequest( - requestCode, - alarmClock, - allowWhileIdle, - exact, - wakeup, - startMillis, - rescheduleOnReboot, - callbackHandle); - } - - final int requestCode; - final boolean alarmClock; - final boolean allowWhileIdle; - final boolean exact; - final boolean wakeup; - final long startMillis; - final boolean rescheduleOnReboot; - final long callbackHandle; - - OneShotRequest( - int requestCode, - boolean alarmClock, - boolean allowWhileIdle, - boolean exact, - boolean wakeup, - long startMillis, - boolean rescheduleOnReboot, - long callbackHandle) { - this.requestCode = requestCode; - this.alarmClock = alarmClock; - this.allowWhileIdle = allowWhileIdle; - this.exact = exact; - this.wakeup = wakeup; - this.startMillis = startMillis; - this.rescheduleOnReboot = rescheduleOnReboot; - this.callbackHandle = callbackHandle; - } - } - - /** A request to schedule a periodic Dart task. */ - static final class PeriodicRequest { - static PeriodicRequest fromJson(JSONArray json) throws JSONException { - int requestCode = json.getInt(0); - boolean exact = json.getBoolean(1); - boolean wakeup = json.getBoolean(2); - long startMillis = json.getLong(3); - long intervalMillis = json.getLong(4); - boolean rescheduleOnReboot = json.getBoolean(5); - long callbackHandle = json.getLong(6); - - return new PeriodicRequest( - requestCode, - exact, - wakeup, - startMillis, - intervalMillis, - rescheduleOnReboot, - callbackHandle); - } - - final int requestCode; - final boolean exact; - final boolean wakeup; - final long startMillis; - final long intervalMillis; - final boolean rescheduleOnReboot; - final long callbackHandle; - - PeriodicRequest( - int requestCode, - boolean exact, - boolean wakeup, - long startMillis, - long intervalMillis, - boolean rescheduleOnReboot, - long callbackHandle) { - this.requestCode = requestCode; - this.exact = exact; - this.wakeup = wakeup; - this.startMillis = startMillis; - this.intervalMillis = intervalMillis; - this.rescheduleOnReboot = rescheduleOnReboot; - this.callbackHandle = callbackHandle; - } - } -} diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java deleted file mode 100644 index 0aa08ed216e0..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/FlutterBackgroundExecutor.java +++ /dev/null @@ -1,218 +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.androidalarmmanager; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.AssetManager; -import android.util.Log; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.dart.DartExecutor; -import io.flutter.embedding.engine.dart.DartExecutor.DartCallback; -import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.JSONMethodCodec; -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.view.FlutterCallbackInformation; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * An background execution abstraction which handles initializing a background isolate running a - * callback dispatcher, used to invoke Dart callbacks while backgrounded. - */ -public class FlutterBackgroundExecutor implements MethodCallHandler { - private static final String TAG = "FlutterBackgroundExecutor"; - private static final String CALLBACK_HANDLE_KEY = "callback_handle"; - - @SuppressWarnings("deprecation") - private static io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback - pluginRegistrantCallback; - - /** - * The {@link MethodChannel} that connects the Android side of this plugin with the background - * Dart isolate that was created by this plugin. - */ - private MethodChannel backgroundChannel; - - private FlutterEngine backgroundFlutterEngine; - - private AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false); - - /** - * Sets the Dart callback handle for the Dart method that is responsible for initializing the - * background Dart isolate, preparing it to receive Dart callback tasks requests. - */ - public static void setCallbackDispatcher(Context context, long callbackHandle) { - SharedPreferences prefs = context.getSharedPreferences(AlarmService.SHARED_PREFERENCES_KEY, 0); - prefs.edit().putLong(CALLBACK_HANDLE_KEY, callbackHandle).apply(); - } - - /** Returns true when the background isolate has started and is ready to handle alarms. */ - public boolean isRunning() { - return isCallbackDispatcherReady.get(); - } - - private void onInitialized() { - isCallbackDispatcherReady.set(true); - AlarmService.onInitialized(); - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - String method = call.method; - if (method.equals("AlarmService.initialized")) { - // This message is sent by the background method channel as soon as the background isolate - // is running. From this point forward, the Android side of this plugin can send - // callback handles through the background method channel, and the Dart side will execute - // the Dart methods corresponding to those callback handles. - onInitialized(); - result.success(true); - } else { - result.notImplemented(); - } - } - - /** - * Starts running a background Dart isolate within a new {@link FlutterEngine} using a previously - * used entrypoint. - * - *

The isolate is configured as follows: - * - *

    - *
  • Bundle Path: {@code io.flutter.view.FlutterMain.findAppBundlePath(context)}. - *
  • Entrypoint: The Dart method used the last time this plugin was initialized in the - * foreground. - *
  • Run args: none. - *
- * - *

Preconditions: - * - *

    - *
  • The given callback must correspond to a registered Dart callback. If the handle does not - * resolve to a Dart callback then this method does nothing. - *
- */ - public void startBackgroundIsolate(Context context) { - if (!isRunning()) { - SharedPreferences p = context.getSharedPreferences(AlarmService.SHARED_PREFERENCES_KEY, 0); - long callbackHandle = p.getLong(CALLBACK_HANDLE_KEY, 0); - startBackgroundIsolate(context, callbackHandle); - } - } - - /** - * Starts running a background Dart isolate within a new {@link FlutterEngine}. - * - *

The isolate is configured as follows: - * - *

    - *
  • Bundle Path: {@code io.flutter.view.FlutterMain.findAppBundlePath(context)}. - *
  • Entrypoint: The Dart method represented by {@code callbackHandle}. - *
  • Run args: none. - *
- * - *

Preconditions: - * - *

    - *
  • The given {@code callbackHandle} must correspond to a registered Dart callback. If the - * handle does not resolve to a Dart callback then this method does nothing. - *
- */ - public void startBackgroundIsolate(Context context, long callbackHandle) { - if (backgroundFlutterEngine != null) { - Log.e(TAG, "Background isolate already started"); - return; - } - - Log.i(TAG, "Starting AlarmService..."); - @SuppressWarnings("deprecation") - String appBundlePath = io.flutter.view.FlutterMain.findAppBundlePath(); - AssetManager assets = context.getAssets(); - if (!isRunning()) { - backgroundFlutterEngine = new FlutterEngine(context); - - // We need to create an instance of `FlutterEngine` before looking up the - // callback. If we don't, the callback cache won't be initialized and the - // lookup will fail. - FlutterCallbackInformation flutterCallback = - FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); - - DartExecutor executor = backgroundFlutterEngine.getDartExecutor(); - initializeMethodChannel(executor); - DartCallback dartCallback = new DartCallback(assets, appBundlePath, flutterCallback); - - executor.executeDartCallback(dartCallback); - - // The pluginRegistrantCallback should only be set in the V1 embedding as - // plugin registration is done via reflection in the V2 embedding. - if (pluginRegistrantCallback != null) { - pluginRegistrantCallback.registerWith(new ShimPluginRegistry(backgroundFlutterEngine)); - } - } - } - - /** - * Executes the desired Dart callback in a background Dart isolate. - * - *

The given {@code intent} should contain a {@code long} extra called "callbackHandle", which - * corresponds to a callback registered with the Dart VM. - */ - public void executeDartCallbackInBackgroundIsolate(Intent intent, final CountDownLatch latch) { - // Grab the handle for the callback associated with this alarm. Pay close - // attention to the type of the callback handle as storing this value in a - // variable of the wrong size will cause the callback lookup to fail. - long callbackHandle = intent.getLongExtra("callbackHandle", 0); - - // If another thread is waiting, then wake that thread when the callback returns a result. - MethodChannel.Result result = null; - if (latch != null) { - result = - new MethodChannel.Result() { - @Override - public void success(Object result) { - latch.countDown(); - } - - @Override - public void error(String errorCode, String errorMessage, Object errorDetails) { - latch.countDown(); - } - - @Override - public void notImplemented() { - latch.countDown(); - } - }; - } - - // Handle the alarm event in Dart. Note that for this plugin, we don't - // care about the method name as we simply lookup and invoke the callback - // provided. - backgroundChannel.invokeMethod( - "invokeAlarmManagerCallback", - new Object[] {callbackHandle, intent.getIntExtra("id", -1)}, - result); - } - - private void initializeMethodChannel(BinaryMessenger isolate) { - // backgroundChannel is the channel responsible for receiving the following messages from - // the background isolate that was setup by this plugin: - // - "AlarmService.initialized" - // - // This channel is also responsible for sending requests from Android to Dart to execute Dart - // callbacks in the background isolate. - backgroundChannel = - new MethodChannel( - isolate, - "plugins.flutter.io/android_alarm_manager_background", - JSONMethodCodec.INSTANCE); - backgroundChannel.setMethodCallHandler(this); - } -} diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/RebootBroadcastReceiver.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/RebootBroadcastReceiver.java deleted file mode 100644 index 9135755863a1..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/RebootBroadcastReceiver.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.androidalarmmanager; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.util.Log; - -/** - * Reschedules background work after the Android device reboots. - * - *

When an Android device reboots, all previously scheduled {@link AlarmManager} timers are - * cleared. - * - *

Timer callbacks registered with the android_alarm_manager plugin can be designated - * "persistent" and therefore, upon device reboot, should be rescheduled for execution. To - * accomplish this rescheduling, {@code RebootBroadcastReceiver} is scheduled by {@link - * AlarmService} to run on {@code BOOT_COMPLETED} and do the rescheduling. - */ -public class RebootBroadcastReceiver extends BroadcastReceiver { - /** - * Invoked by the OS whenever a broadcast is received by this app. - * - *

If the broadcast's action is {@code BOOT_COMPLETED} then this {@code - * RebootBroadcastReceiver} reschedules all persistent timer callbacks. That rescheduling work is - * handled by {@link AlarmService#reschedulePersistentAlarms(Context)}. - */ - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - Log.i("AlarmService", "Rescheduling after boot!"); - AlarmService.reschedulePersistentAlarms(context); - } - } - - /** - * Schedules this {@code RebootBroadcastReceiver} to be run whenever the Android device reboots. - */ - public static void enableRescheduleOnReboot(Context context) { - scheduleOnReboot(context, PackageManager.COMPONENT_ENABLED_STATE_ENABLED); - } - - /** - * Unschedules this {@code RebootBroadcastReceiver} to be run whenever the Android device reboots. - * This {@code RebootBroadcastReceiver} will no longer be run upon reboot. - */ - public static void disableRescheduleOnReboot(Context context) { - scheduleOnReboot(context, PackageManager.COMPONENT_ENABLED_STATE_DISABLED); - } - - private static void scheduleOnReboot(Context context, int state) { - ComponentName receiver = new ComponentName(context, RebootBroadcastReceiver.class); - PackageManager pm = context.getPackageManager(); - pm.setComponentEnabledSetting(receiver, state, PackageManager.DONT_KILL_APP); - } -} diff --git a/packages/android_alarm_manager/android_alarm_manager_android.iml b/packages/android_alarm_manager/android_alarm_manager_android.iml deleted file mode 100644 index 0ebb6c9fe763..000000000000 --- a/packages/android_alarm_manager/android_alarm_manager_android.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/README.md b/packages/android_alarm_manager/example/README.md deleted file mode 100644 index 0df1ed9fb4ec..000000000000 --- a/packages/android_alarm_manager/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# android_alarm_manager_example - -Demonstrates how to use the android_alarm_manager plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/android_alarm_manager/example/android.iml b/packages/android_alarm_manager/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/android_alarm_manager/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/android/app/build.gradle b/packages/android_alarm_manager/example/android/app/build.gradle deleted file mode 100644 index 9722ec280205..000000000000 --- a/packages/android_alarm_manager/example/android/app/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.androidalarmmanagerexample" - minSdkVersion 16 - 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 - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - testImplementation "com.google.truth:truth:1.0" - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - api 'androidx.test:core:1.2.0' -} diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java deleted file mode 100644 index a841a239d3af..000000000000 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java +++ /dev/null @@ -1,66 +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.androidalarmmanagerexample; - -import static androidx.test.espresso.Espresso.pressBackUnconditionally; -import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; -import static androidx.test.espresso.flutter.action.FlutterActions.click; -import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; -import static org.junit.Assert.assertEquals; - -import android.content.Context; -import android.content.SharedPreferences; -import androidx.test.InstrumentationRegistry; -import androidx.test.core.app.ActivityScenario; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.ActivityTestRule; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class BackgroundExecutionTest { - private SharedPreferences prefs; - static final String COUNT_KEY = "flutter.count"; - - @Rule - public ActivityTestRule myActivityTestRule = - new ActivityTestRule<>(DriverExtensionActivity.class, true, false); - - @Before - public void setUp() throws Exception { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE); - prefs.edit().putLong(COUNT_KEY, 0).apply(); - - ActivityScenario.launch(DriverExtensionActivity.class); - } - - @Ignore("Disabled due to flake: https://github.com/flutter/flutter/issues/88837") - @Test - public void startBackgroundIsolate() throws Exception { - - // Register a one shot alarm which will go off in ~5 seconds. - onFlutterWidget(withValueKey("RegisterOneShotAlarm")).perform(click()); - - // The alarm count should be 0 after installation. - assertEquals(prefs.getLong(COUNT_KEY, -1), 0); - - // Close the application to background it. - pressBackUnconditionally(); - - // The alarm should eventually fire, wake up the application, create a - // background isolate, and then increment the counter in the shared - // preferences. Timeout after 20s, just to be safe. - int tries = 0; - while ((prefs.getLong(COUNT_KEY, -1) == 0) && (tries < 200)) { - Thread.sleep(100); - ++tries; - } - assertEquals(prefs.getLong(COUNT_KEY, -1), 1); - } -} diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java deleted file mode 100644 index a5bb72415f14..000000000000 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java +++ /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. - -package io.flutter.plugins.androidalarmmanagerexample; - -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, true, false); -} diff --git a/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index e826cdd83ac7..000000000000 --- a/packages/android_alarm_manager/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 2fef38483800..000000000000 --- a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/android/gradle.properties b/packages/android_alarm_manager/example/android/gradle.properties deleted file mode 100644 index b6e61b62b903..000000000000 --- a/packages/android_alarm_manager/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -android.enableJetifier=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/android_alarm_manager/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/android_alarm_manager/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 2819f022f1fd..000000000000 --- a/packages/android_alarm_manager/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#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-4.10.2-all.zip diff --git a/packages/android_alarm_manager/example/android_alarm_manager_example.iml b/packages/android_alarm_manager/example/android_alarm_manager_example.iml deleted file mode 100644 index d6ba21bef85f..000000000000 --- a/packages/android_alarm_manager/example/android_alarm_manager_example.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/android_alarm_manager_example_android.iml b/packages/android_alarm_manager/example/android_alarm_manager_example_android.iml deleted file mode 100644 index 0ca70ed93eaf..000000000000 --- a/packages/android_alarm_manager/example/android_alarm_manager_example_android.iml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/integration_test/android_alarm_manager_test.dart b/packages/android_alarm_manager/example/integration_test/android_alarm_manager_test.dart deleted file mode 100644 index 37b5659f09f4..000000000000 --- a/packages/android_alarm_manager/example/integration_test/android_alarm_manager_test.dart +++ /dev/null @@ -1,108 +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 'dart:async'; -import 'dart:io'; -import 'package:android_alarm_manager_example/main.dart' as app; -import 'package:android_alarm_manager/android_alarm_manager.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_driver/driver_extension.dart'; -import 'package:path_provider/path_provider.dart'; - -// From https://flutter.dev/docs/cookbook/persistence/reading-writing-files -Future get _localPath async { - final Directory directory = await getTemporaryDirectory(); - return directory.path; -} - -Future get _localFile async { - final String path = await _localPath; - return File('$path/counter.txt'); -} - -Future writeCounter(int counter) async { - final File file = await _localFile; - - // Write the file. - return file.writeAsString('$counter'); -} - -Future readCounter() async { - try { - final File file = await _localFile; - - // Read the file. - final String contents = await file.readAsString(); - - return int.parse(contents); - // ignore: unused_catch_clause - } on FileSystemException catch (e) { - // If encountering an error, return 0. - return 0; - } -} - -Future incrementCounter() async { - final int value = await readCounter(); - await writeCounter(value + 1); -} - -void appMain() { - enableFlutterDriverExtension(); - app.main(); -} - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() async { - await AndroidAlarmManager.initialize(); - }); - - group('oneshot', () { - testWidgets('cancelled before it fires', (WidgetTester tester) async { - final int alarmId = 0; - final int startingValue = await readCounter(); - await AndroidAlarmManager.oneShot( - const Duration(seconds: 1), alarmId, incrementCounter); - expect(await AndroidAlarmManager.cancel(alarmId), isTrue); - await Future.delayed(const Duration(seconds: 4)); - expect(await readCounter(), startingValue); - }); - - testWidgets('cancelled after it fires', (WidgetTester tester) async { - final int alarmId = 1; - final int startingValue = await readCounter(); - await AndroidAlarmManager.oneShot( - const Duration(seconds: 1), alarmId, incrementCounter, - exact: true, wakeup: true); - await Future.delayed(const Duration(seconds: 2)); - // poll until file is updated - while (await readCounter() == startingValue) { - await Future.delayed(const Duration(seconds: 1)); - } - expect(await readCounter(), startingValue + 1); - expect(await AndroidAlarmManager.cancel(alarmId), isTrue); - }); - }); - - testWidgets('periodic', (WidgetTester tester) async { - final int alarmId = 2; - final int startingValue = await readCounter(); - await AndroidAlarmManager.periodic( - const Duration(seconds: 1), alarmId, incrementCounter, - wakeup: true, exact: true); - // poll until file is updated - while (await readCounter() < startingValue + 2) { - await Future.delayed(const Duration(seconds: 1)); - } - expect(await readCounter(), startingValue + 2); - expect(await AndroidAlarmManager.cancel(alarmId), isTrue); - await Future.delayed(const Duration(seconds: 3)); - expect(await readCounter(), startingValue + 2); - }); -} diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart deleted file mode 100644 index 75648b8ded5f..000000000000 --- a/packages/android_alarm_manager/example/lib/main.dart +++ /dev/null @@ -1,155 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:isolate'; -import 'dart:math'; -import 'dart:ui'; - -import 'package:android_alarm_manager/android_alarm_manager.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/material.dart'; - -/// The [SharedPreferences] key to access the alarm fire count. -const String countKey = 'count'; - -/// The name associated with the UI isolate's [SendPort]. -const String isolateName = 'isolate'; - -/// A port used to communicate from a background isolate to the UI isolate. -final ReceivePort port = ReceivePort(); - -/// Global [SharedPreferences] object. -late SharedPreferences prefs; - -Future main() async { - // TODO(bkonyi): uncomment - WidgetsFlutterBinding.ensureInitialized(); - - // Register the UI isolate's SendPort to allow for communication from the - // background isolate. - IsolateNameServer.registerPortWithName( - port.sendPort, - isolateName, - ); - prefs = await SharedPreferences.getInstance(); - if (!prefs.containsKey(countKey)) { - await prefs.setInt(countKey, 0); - } - runApp(AlarmManagerExampleApp()); -} - -/// Example app for Espresso plugin. -class AlarmManagerExampleApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - home: _AlarmHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class _AlarmHomePage extends StatefulWidget { - _AlarmHomePage({Key? key, required this.title}) : super(key: key); - final String title; - - @override - _AlarmHomePageState createState() => _AlarmHomePageState(); -} - -class _AlarmHomePageState extends State<_AlarmHomePage> { - int _counter = 0; - - @override - void initState() { - super.initState(); - AndroidAlarmManager.initialize(); - - // Register for events from the background isolate. These messages will - // always coincide with an alarm firing. - port.listen((_) async => await _incrementCounter()); - } - - Future _incrementCounter() async { - print('Increment counter!'); - - // Ensure we've loaded the updated count from the background isolate. - await prefs.reload(); - - setState(() { - _counter++; - }); - } - - // The background - static SendPort? uiSendPort; - - // The callback for our alarm - static Future callback() async { - print('Alarm fired!'); - - // Get the previous cached count and increment it. - final prefs = await SharedPreferences.getInstance(); - int currentCount = prefs.getInt(countKey) ?? 0; - await prefs.setInt(countKey, currentCount + 1); - - // This will be null if we're running in the background. - uiSendPort ??= IsolateNameServer.lookupPortByName(isolateName); - uiSendPort?.send(null); - } - - @override - Widget build(BuildContext context) { - final textStyle = Theme.of(context).textTheme.headline4; - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Alarm fired $_counter times', - style: textStyle, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Total alarms fired: ', - style: textStyle, - ), - Text( - prefs.getInt(countKey).toString(), - key: ValueKey('BackgroundCountText'), - style: textStyle, - ), - ], - ), - ElevatedButton( - child: Text( - 'Schedule OneShot Alarm', - ), - key: ValueKey('RegisterOneShotAlarm'), - onPressed: () async { - await AndroidAlarmManager.oneShot( - const Duration(seconds: 5), - // Ensure we have a unique alarm ID. - Random().nextInt(pow(2, 31).toInt()), - callback, - exact: true, - wakeup: true, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml deleted file mode 100644 index 821440c49659..000000000000 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: android_alarm_manager_example -description: Demonstrates how to use the android_alarm_manager plugin. -publish_to: none - -environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - android_alarm_manager: - # When depending on this package from a real application you should use: - # android_alarm_manager: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - shared_preferences: ^2.0.0 - integration_test: - sdk: flutter - path_provider: ^2.0.0 - -dev_dependencies: - espresso: ^0.0.1+3 - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/android_alarm_manager/example/test_driver/integration_test.dart b/packages/android_alarm_manager/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/android_alarm_manager/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart deleted file mode 100644 index e4e3855933ca..000000000000 --- a/packages/android_alarm_manager/lib/android_alarm_manager.dart +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -const String _backgroundName = - 'plugins.flutter.io/android_alarm_manager_background'; - -// This is the entrypoint for the background isolate. Since we can only enter -// an isolate once, we setup a MethodChannel to listen for method invocations -// from the native portion of the plugin. This allows for the plugin to perform -// any necessary processing in Dart (e.g., populating a custom object) before -// invoking the provided callback. -void _alarmManagerCallbackDispatcher() { - // Initialize state necessary for MethodChannels. - WidgetsFlutterBinding.ensureInitialized(); - - const MethodChannel _channel = - MethodChannel(_backgroundName, JSONMethodCodec()); - // This is where the magic happens and we handle background events from the - // native portion of the plugin. - _channel.setMethodCallHandler((MethodCall call) async { - final dynamic args = call.arguments; - final CallbackHandle handle = CallbackHandle.fromRawHandle(args[0]); - - // PluginUtilities.getCallbackFromHandle performs a lookup based on the - // callback handle and returns a tear-off of the original callback. - final Function? closure = PluginUtilities.getCallbackFromHandle(handle); - - if (closure == null) { - print('Fatal: could not find callback'); - exit(-1); - } - - // ignore: inference_failure_on_function_return_type - if (closure is Function()) { - closure(); - // ignore: inference_failure_on_function_return_type - } else if (closure is Function(int)) { - final int id = args[1]; - closure(id); - } - }); - - // Once we've finished initializing, let the native portion of the plugin - // know that it can start scheduling alarms. - _channel.invokeMethod('AlarmService.initialized'); -} - -// A lambda that returns the current instant in the form of a [DateTime]. -typedef DateTime _Now(); -// A lambda that gets the handle for the given [callback]. -typedef CallbackHandle? _GetCallbackHandle(Function callback); - -/// A Flutter plugin for registering Dart callbacks with the Android -/// AlarmManager service. -/// -/// See the example/ directory in this package for sample usage. -class AndroidAlarmManager { - static const String _channelName = 'plugins.flutter.io/android_alarm_manager'; - static MethodChannel _channel = - const MethodChannel(_channelName, JSONMethodCodec()); - // Function used to get the current time. It's [DateTime.now] by default. - static _Now _now = () => DateTime.now(); - // Callback used to get the handle for a callback. It's - // [PluginUtilities.getCallbackHandle] by default. - static _GetCallbackHandle _getCallbackHandle = - (Function callback) => PluginUtilities.getCallbackHandle(callback); - - /// This is exposed for the unit tests. It should not be accessed by users of - /// the plugin. - @visibleForTesting - static void setTestOverides( - {_Now? now, _GetCallbackHandle? getCallbackHandle}) { - _now = (now ?? _now); - _getCallbackHandle = (getCallbackHandle ?? _getCallbackHandle); - } - - /// Starts the [AndroidAlarmManager] service. This must be called before - /// setting any alarms. - /// - /// Returns a [Future] that resolves to `true` on success and `false` on - /// failure. - static Future initialize() async { - final CallbackHandle? handle = - _getCallbackHandle(_alarmManagerCallbackDispatcher); - if (handle == null) { - return false; - } - final bool? r = await _channel.invokeMethod( - 'AlarmService.start', [handle.toRawHandle()]); - return r ?? false; - } - - /// Schedules a one-shot timer to run `callback` after time `delay`. - /// - /// The `callback` will run whether or not the main application is running or - /// in the foreground. It will run in the Isolate owned by the - /// AndroidAlarmManager service. - /// - /// `callback` must be either a top-level function or a static method from a - /// class. - /// - /// `callback` can be `Function()` or `Function(int)` - /// - /// The timer is uniquely identified by `id`. Calling this function again - /// with the same `id` will cancel and replace the existing timer. - /// - /// `id` will passed to `callback` if it is of type `Function(int)` - /// - /// If `alarmClock` is passed as `true`, the timer will be created with - /// Android's `AlarmManagerCompat.setAlarmClock`. - /// - /// If `allowWhileIdle` is passed as `true`, the timer will be created with - /// Android's `AlarmManagerCompat.setExactAndAllowWhileIdle` or - /// `AlarmManagerCompat.setAndAllowWhileIdle`. - /// - /// If `exact` is passed as `true`, the timer will be created with Android's - /// `AlarmManagerCompat.setExact`. When `exact` is `false` (the default), the - /// timer will be created with `AlarmManager.set`. - /// - /// If `wakeup` is passed as `true`, the device will be woken up when the - /// alarm fires. If `wakeup` is false (the default), the device will not be - /// woken up to service the alarm. - /// - /// If `rescheduleOnReboot` is passed as `true`, the alarm will be persisted - /// across reboots. If `rescheduleOnReboot` is false (the default), the alarm - /// will not be rescheduled after a reboot and will not be executed. - /// - /// Returns a [Future] that resolves to `true` on success and `false` on - /// failure. - static Future oneShot( - Duration delay, - int id, - Function callback, { - bool alarmClock = false, - bool allowWhileIdle = false, - bool exact = false, - bool wakeup = false, - bool rescheduleOnReboot = false, - }) => - oneShotAt( - _now().add(delay), - id, - callback, - alarmClock: alarmClock, - allowWhileIdle: allowWhileIdle, - exact: exact, - wakeup: wakeup, - rescheduleOnReboot: rescheduleOnReboot, - ); - - /// Schedules a one-shot timer to run `callback` at `time`. - /// - /// The `callback` will run whether or not the main application is running or - /// in the foreground. It will run in the Isolate owned by the - /// AndroidAlarmManager service. - /// - /// `callback` must be either a top-level function or a static method from a - /// class. - /// - /// `callback` can be `Function()` or `Function(int)` - /// - /// The timer is uniquely identified by `id`. Calling this function again - /// with the same `id` will cancel and replace the existing timer. - /// - /// `id` will passed to `callback` if it is of type `Function(int)` - /// - /// If `alarmClock` is passed as `true`, the timer will be created with - /// Android's `AlarmManagerCompat.setAlarmClock`. - /// - /// If `allowWhileIdle` is passed as `true`, the timer will be created with - /// Android's `AlarmManagerCompat.setExactAndAllowWhileIdle` or - /// `AlarmManagerCompat.setAndAllowWhileIdle`. - /// - /// If `exact` is passed as `true`, the timer will be created with Android's - /// `AlarmManagerCompat.setExact`. When `exact` is `false` (the default), the - /// timer will be created with `AlarmManager.set`. - /// - /// If `wakeup` is passed as `true`, the device will be woken up when the - /// alarm fires. If `wakeup` is false (the default), the device will not be - /// woken up to service the alarm. - /// - /// If `rescheduleOnReboot` is passed as `true`, the alarm will be persisted - /// across reboots. If `rescheduleOnReboot` is false (the default), the alarm - /// will not be rescheduled after a reboot and will not be executed. - /// - /// Returns a [Future] that resolves to `true` on success and `false` on - /// failure. - static Future oneShotAt( - DateTime time, - int id, - Function callback, { - bool alarmClock = false, - bool allowWhileIdle = false, - bool exact = false, - bool wakeup = false, - bool rescheduleOnReboot = false, - }) async { - // ignore: inference_failure_on_function_return_type - assert(callback is Function() || callback is Function(int)); - assert(id.bitLength < 32); - final int startMillis = time.millisecondsSinceEpoch; - final CallbackHandle? handle = _getCallbackHandle(callback); - if (handle == null) { - return false; - } - final bool? r = - await _channel.invokeMethod('Alarm.oneShotAt', [ - id, - alarmClock, - allowWhileIdle, - exact, - wakeup, - startMillis, - rescheduleOnReboot, - handle.toRawHandle(), - ]); - return r ?? false; - } - - /// Schedules a repeating timer to run `callback` with period `duration`. - /// - /// The `callback` will run whether or not the main application is running or - /// in the foreground. It will run in the Isolate owned by the - /// AndroidAlarmManager service. - /// - /// `callback` must be either a top-level function or a static method from a - /// class. - /// - /// `callback` can be `Function()` or `Function(int)` - /// - /// The repeating timer is uniquely identified by `id`. Calling this function - /// again with the same `id` will cancel and replace the existing timer. - /// - /// `id` will passed to `callback` if it is of type `Function(int)` - /// - /// If `startAt` is passed, the timer will first go off at that time and - /// subsequently run with period `duration`. - /// - /// If `exact` is passed as `true`, the timer will be created with Android's - /// `AlarmManager.setRepeating`. When `exact` is `false` (the default), the - /// timer will be created with `AlarmManager.setInexactRepeating`. - /// - /// If `wakeup` is passed as `true`, the device will be woken up when the - /// alarm fires. If `wakeup` is false (the default), the device will not be - /// woken up to service the alarm. - /// - /// If `rescheduleOnReboot` is passed as `true`, the alarm will be persisted - /// across reboots. If `rescheduleOnReboot` is false (the default), the alarm - /// will not be rescheduled after a reboot and will not be executed. - /// - /// Returns a [Future] that resolves to `true` on success and `false` on - /// failure. - static Future periodic( - Duration duration, - int id, - Function callback, { - DateTime? startAt, - bool exact = false, - bool wakeup = false, - bool rescheduleOnReboot = false, - }) async { - // ignore: inference_failure_on_function_return_type - assert(callback is Function() || callback is Function(int)); - assert(id.bitLength < 32); - final int now = _now().millisecondsSinceEpoch; - final int period = duration.inMilliseconds; - final int first = - startAt != null ? startAt.millisecondsSinceEpoch : now + period; - final CallbackHandle? handle = _getCallbackHandle(callback); - if (handle == null) { - return false; - } - final bool? r = await _channel.invokeMethod( - 'Alarm.periodic', [ - id, - exact, - wakeup, - first, - period, - rescheduleOnReboot, - handle.toRawHandle() - ]); - return r ?? false; - } - - /// Cancels a timer. - /// - /// If a timer has been scheduled with `id`, then this function will cancel - /// it. - /// - /// Returns a [Future] that resolves to `true` on success and `false` on - /// failure. - static Future cancel(int id) async { - final bool? r = - await _channel.invokeMethod('Alarm.cancel', [id]); - return r ?? false; - } -} diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml deleted file mode 100644 index 450fb914b739..000000000000 --- a/packages/android_alarm_manager/pubspec.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: android_alarm_manager -description: Flutter plugin for accessing the Android AlarmManager service, and - running Dart code in the background when alarms fire. -repository: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+android_alarm_manager%22 -version: 2.0.2 - -environment: - sdk: '>=2.12.0 <3.0.0' - flutter: ">=1.20.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.androidalarmmanager - pluginClass: AndroidAlarmManagerPlugin - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/android_alarm_manager/test/android_alarm_manager_test.dart b/packages/android_alarm_manager/test/android_alarm_manager_test.dart deleted file mode 100644 index 908bb957c0f2..000000000000 --- a/packages/android_alarm_manager/test/android_alarm_manager_test.dart +++ /dev/null @@ -1,201 +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:android_alarm_manager/android_alarm_manager.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - String invalidCallback(String foo) => foo; - void validCallback(int id) => null; - - const MethodChannel testChannel = MethodChannel( - 'plugins.flutter.io/android_alarm_manager', JSONMethodCodec()); - TestWidgetsFlutterBinding.ensureInitialized(); - - setUpAll(() { - testChannel.setMockMethodCallHandler((MethodCall call) => null); - }); - - test('${AndroidAlarmManager.initialize}', () async { - testChannel.setMockMethodCallHandler((MethodCall call) async { - assert(call.method == 'AlarmService.start'); - return true; - }); - - final bool initialized = await AndroidAlarmManager.initialize(); - - expect(initialized, isTrue); - }); - - group('${AndroidAlarmManager.oneShotAt}', () { - test('validates input', () async { - final DateTime validTime = DateTime.utc(1993); - final int validId = 1; - - // Callback should take a single int param. - await expectLater( - () => AndroidAlarmManager.oneShotAt( - validTime, validId, invalidCallback), - throwsAssertionError); - - // ID should be less than 32 bits. - await expectLater( - () => AndroidAlarmManager.oneShotAt( - validTime, 2147483648, validCallback), - throwsAssertionError); - }); - - test('sends arguments to the platform', () async { - final DateTime alarm = DateTime(1993); - const int rawHandle = 4; - AndroidAlarmManager.setTestOverides( - getCallbackHandle: (Function _) => - CallbackHandle.fromRawHandle(rawHandle)); - - final int id = 1; - final bool alarmClock = true; - final bool allowWhileIdle = true; - final bool exact = true; - final bool wakeup = true; - final bool rescheduleOnReboot = true; - - testChannel.setMockMethodCallHandler((MethodCall call) async { - expect(call.method, 'Alarm.oneShotAt'); - expect(call.arguments[0], id); - expect(call.arguments[1], alarmClock); - expect(call.arguments[2], allowWhileIdle); - expect(call.arguments[3], exact); - expect(call.arguments[4], wakeup); - expect(call.arguments[5], alarm.millisecondsSinceEpoch); - expect(call.arguments[6], rescheduleOnReboot); - expect(call.arguments[7], rawHandle); - return true; - }); - - final bool result = await AndroidAlarmManager.oneShotAt( - alarm, id, validCallback, - alarmClock: alarmClock, - allowWhileIdle: allowWhileIdle, - exact: exact, - wakeup: wakeup, - rescheduleOnReboot: rescheduleOnReboot); - - expect(result, isTrue); - }); - }); - - test('${AndroidAlarmManager.oneShot} calls through to oneShotAt', () async { - final DateTime now = DateTime(1993); - const int rawHandle = 4; - AndroidAlarmManager.setTestOverides( - now: () => now, - getCallbackHandle: (Function _) => - CallbackHandle.fromRawHandle(rawHandle)); - - const Duration alarm = Duration(seconds: 1); - final int id = 1; - final bool alarmClock = true; - final bool allowWhileIdle = true; - final bool exact = true; - final bool wakeup = true; - final bool rescheduleOnReboot = true; - - testChannel.setMockMethodCallHandler((MethodCall call) async { - expect(call.method, 'Alarm.oneShotAt'); - expect(call.arguments[0], id); - expect(call.arguments[1], alarmClock); - expect(call.arguments[2], allowWhileIdle); - expect(call.arguments[3], exact); - expect(call.arguments[4], wakeup); - expect( - call.arguments[5], now.millisecondsSinceEpoch + alarm.inMilliseconds); - expect(call.arguments[6], rescheduleOnReboot); - expect(call.arguments[7], rawHandle); - return true; - }); - - final bool result = await AndroidAlarmManager.oneShot( - alarm, id, validCallback, - alarmClock: alarmClock, - allowWhileIdle: allowWhileIdle, - exact: exact, - wakeup: wakeup, - rescheduleOnReboot: rescheduleOnReboot); - - expect(result, isTrue); - }); - - group('${AndroidAlarmManager.periodic}', () { - test('validates input', () async { - const Duration validDuration = Duration(seconds: 0); - final int validId = 1; - - // Callback should take a single int param. - await expectLater( - () => AndroidAlarmManager.periodic( - validDuration, validId, invalidCallback), - throwsAssertionError); - - // ID should be less than 32 bits. - await expectLater( - () => AndroidAlarmManager.periodic( - validDuration, 2147483648, validCallback), - throwsAssertionError); - }); - - test('sends arguments through to the platform', () async { - final DateTime now = DateTime(1993); - const int rawHandle = 4; - AndroidAlarmManager.setTestOverides( - now: () => now, - getCallbackHandle: (Function _) => - CallbackHandle.fromRawHandle(rawHandle)); - - final int id = 1; - final bool exact = true; - final bool wakeup = true; - final bool rescheduleOnReboot = true; - const Duration period = Duration(seconds: 1); - - testChannel.setMockMethodCallHandler((MethodCall call) async { - expect(call.method, 'Alarm.periodic'); - expect(call.arguments[0], id); - expect(call.arguments[1], exact); - expect(call.arguments[2], wakeup); - expect(call.arguments[3], - (now.millisecondsSinceEpoch + period.inMilliseconds)); - expect(call.arguments[4], period.inMilliseconds); - expect(call.arguments[5], rescheduleOnReboot); - expect(call.arguments[6], rawHandle); - return true; - }); - - final bool result = await AndroidAlarmManager.periodic( - period, - id, - (int id) => null, - exact: exact, - wakeup: wakeup, - rescheduleOnReboot: rescheduleOnReboot, - ); - - expect(result, isTrue); - }); - }); - - test('${AndroidAlarmManager.cancel}', () async { - final int id = 1; - testChannel.setMockMethodCallHandler((MethodCall call) async { - assert(call.method == 'Alarm.cancel' && call.arguments[0] == id); - return true; - }); - - final bool canceled = await AndroidAlarmManager.cancel(id); - - expect(canceled, isTrue); - }); -} diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md deleted file mode 100644 index 79eafe70e821..000000000000 --- a/packages/android_intent/CHANGELOG.md +++ /dev/null @@ -1,202 +0,0 @@ -## NEXT - -* Remove references to the V1 Android embedding. -* Updated Android lint settings. -* Specify Java 8 for Android build. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. - -## 0.3.7+8 - -* Update Flutter SDK constraint. - -## 0.3.7+7 - -* Update Dart SDK constraint in example. - -## 0.3.7+6 - -* Update android compileSdkVersion to 29. - -## 0.3.7+5 - -* Android Code Inspection and Clean up. - -## 0.3.7+4 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.3.7+3 - -* Update the `platform` package dependency to resolve the conflict with the latest flutter. - -## 0.3.7+2 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.3.7+1 - -* Fix CocoaPods podspec lint warnings. - -## 0.3.7 - -* Add a `Future canResolveActivity` method to the AndroidIntent class. It - can be used to determine whether a device supports a particular intent or has - an app installed that can resolve it. It is based on PackageManager - [resolveActivity](https://developer.android.com/reference/android/content/pm/PackageManager#resolveActivity(android.content.Intent,%20int)). - -## 0.3.6+1 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Bump the minimum Dart version to 2.3.0. -* Uses Darts spread operator to build plugin arguments internally. -* Remove deprecated API usage warning in AndroidIntentPlugin.java. -* Migrates the Android example to V2 embedding. - -## 0.3.6 - -* Marks the `action` parameter as optional -* Adds an assertion to ensure the intent receives an action, component or both. - -## 0.3.5+1 - -* Make the pedantic dev_dependency explicit. - -## 0.3.5 - -* Add support for [setType](https://developer.android.com/reference/android/content/Intent.html#setType(java.lang.String)) and [setDataAndType](https://developer.android.com/reference/android/content/Intent.html#setDataAndType(android.net.Uri,%20java.lang.String)) parameters. - -## 0.3.4+8 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.3.4+7 - -* Fix pedantic linter errors. - -## 0.3.4+6 - -* Add missing DartDocs for public members. - -## 0.3.4+5 - -* Remove AndroidX warning. - -## 0.3.4+4 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.3.4+3 - -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. - -## 0.3.4+2 - -* Fix resolveActivity not respecting the provided componentName. - -## 0.3.4+1 - -* Fix minor lints in the Java platform code. -* Add smoke e2e tests for the V2 embedding. -* Fully migrate the example app to AndroidX. - -## 0.3.4 - -* Migrate the plugin to use the V2 Android engine embedding. This shouldn't - affect existing functionality. Plugin authors who use the V2 embedding can now - instantiate the plugin and expect that it correctly responds to app lifecycle - changes. - -## 0.3.3+3 - -* Define clang module for iOS. - -## 0.3.3+2 - -* Update and migrate iOS example project. - -## 0.3.3+1 - -* Added "action_application_details_settings" action to open application info settings . - -## 0.3.3 - -* Added "flags" option to call intent.addFlags(int) in native. - -## 0.3.2 - -* Added "action_location_source_settings" action to start Location Settings Activity. - -## 0.3.1+1 - -* Fix Gradle version. - -## 0.3.1 - -* Add a new componentName parameter to help the intent resolution. - -## 0.3.0+2 - -* Bump the minimum Flutter version to 1.2.0. -* Add template type parameter to `invokeMethod` calls. - -## 0.3.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.3.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.2.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.3 - -* Add FLT prefix to iOS types. - -## 0.0.2 - -* Add support for transferring structured Dart values into Android Intent - instances as extra Bundle data. - -## 0.0.1 - -* Initial release diff --git a/packages/android_intent/README.md b/packages/android_intent/README.md deleted file mode 100644 index 0ad1117daa0c..000000000000 --- a/packages/android_intent/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Android Intent Plugin for Flutter - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`android_intent_plus`](https://pub.dev/packages/android_intent_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -This plugin allows Flutter apps to launch arbitrary intents when the platform -is Android. If the plugin is invoked on iOS, it will crash your app. In checked -mode, we assert that the platform should be Android. - - -Use it by specifying action, category, data and extra arguments for the intent. -It does not support returning the result of the launched activity. Sample usage: - -```dart -if (Platform.isAndroid) { - AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: 'https://play.google.com/store/apps/details?' - 'id=com.google.android.apps.myapp', - arguments: {'authAccount': currentUserEmail}, - ); - await intent.launch(); -} -``` - -See documentation on the AndroidIntent class for details on each parameter. - -Action parameter can be any action including a custom class name to be invoked. -If a standard android action is required, the recommendation is to add support -for it in the plugin and use an action constant to refer to it. For instance: - -`'action_view'` translates to `android.os.Intent.ACTION_VIEW` - -`'action_location_source_settings'` translates to `android.settings.LOCATION_SOURCE_SETTINGS` - -`'action_application_details_settings'` translates to `android.settings.ACTION_APPLICATION_DETAILS_SETTINGS` - -```dart -if (Platform.isAndroid) { - final AndroidIntent intent = AndroidIntent( - action: 'action_application_details_settings', - data: 'package:com.example.app', // replace com.example.app with your applicationId - ); - await intent.launch(); -} - -``` - -Feel free to add support for additional Android intents. - -The Dart values supported for the arguments parameter, and their corresponding -Android values, are listed [here](https://flutter.dev/docs/development/platform-integration/platform-channels#codec). -On the Android side, the arguments are used to populate an Android `Bundle` -instance. This process currently restricts the use of lists to homogeneous lists -of integers or strings. - -> Note that a similar method does not currently exist for iOS. Instead, the -[url_launcher](https://pub.dev/packages/url_launcher) plugin -can be used for deep linking. Url launcher can also be used for creating -ACTION_VIEW intents for Android, however this intent plugin also allows -clients to set extra parameters for the intent. - -## 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/android_intent/analysis_options.yaml b/packages/android_intent/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/android_intent/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle deleted file mode 100644 index f0af1602dbb1..000000000000 --- a/packages/android_intent/android/build.gradle +++ /dev/null @@ -1,63 +0,0 @@ -group 'io.flutter.plugins.androidintent' -version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - compileOnly 'androidx.annotation:annotation:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' - testImplementation 'androidx.test:core:1.0.0' - testImplementation 'org.robolectric:robolectric:4.3' -} diff --git a/packages/android_intent/android/gradle/wrapper/gradle-wrapper.properties b/packages/android_intent/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 4e974715fd7b..000000000000 --- a/packages/android_intent/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/android_intent/android/settings.gradle b/packages/android_intent/android/settings.gradle deleted file mode 100644 index 6fdf24a6a036..000000000000 --- a/packages/android_intent/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'android_intent' diff --git a/packages/android_intent/android/src/main/AndroidManifest.xml b/packages/android_intent/android/src/main/AndroidManifest.xml deleted file mode 100644 index df6242dcc660..000000000000 --- a/packages/android_intent/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java deleted file mode 100644 index 883d05922874..000000000000 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.androidintent; - -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; - -/** - * Plugin implementation that uses the new {@code io.flutter.embedding} package. - * - *

Instantiate this in an add to app scenario to gracefully handle activity and context changes. - */ -public final class AndroidIntentPlugin implements FlutterPlugin, ActivityAware { - private final IntentSender sender; - private final MethodCallHandlerImpl impl; - - /** - * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. - * - *

See {@code io.flutter.plugins.androidintentexample.MainActivity} for an example. - */ - public AndroidIntentPlugin() { - sender = new IntentSender(/*activity=*/ null, /*applicationContext=*/ null); - impl = new MethodCallHandlerImpl(sender); - } - - /** - * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} - * package. - * - *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link AndroidIntentPlugin}. - */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); - MethodCallHandlerImpl impl = new MethodCallHandlerImpl(sender); - impl.startListening(registrar.messenger()); - } - - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - sender.setApplicationContext(binding.getApplicationContext()); - sender.setActivity(null); - impl.startListening(binding.getBinaryMessenger()); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - sender.setApplicationContext(null); - sender.setActivity(null); - impl.stopListening(); - } - - @Override - public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - sender.setActivity(binding.getActivity()); - } - - @Override - public void onDetachedFromActivity() { - sender.setActivity(null); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity(); - } - - @Override - public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - onAttachedToActivity(binding); - } -} diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java deleted file mode 100644 index 2c05a914c888..000000000000 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/IntentSender.java +++ /dev/null @@ -1,168 +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.androidintent; - -import android.app.Activity; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import androidx.annotation.Nullable; - -/** Forms and launches intents. */ -public final class IntentSender { - private static final String TAG = "IntentSender"; - - @Nullable private Activity activity; - @Nullable private Context applicationContext; - - /** - * Caches the given {@code activity} and {@code applicationContext} to use for sending intents - * later. - * - *

Either may be null initially, but at least {@code applicationContext} should be set before - * calling {@link #send}. - * - *

See also {@link #setActivity}, {@link #setApplicationContext}, and {@link #send}. - */ - public IntentSender(@Nullable Activity activity, @Nullable Context applicationContext) { - this.activity = activity; - this.applicationContext = applicationContext; - } - - /** - * Creates and launches an intent with the given params using the cached {@link Activity} and - * {@link Context}. - * - *

This will fail to create and send the intent if {@code applicationContext} hasn't been set - * at the time of calling. - * - *

This uses {@code activity} to start the intent whenever it's not null. Otherwise it falls - * back to {@code applicationContext} and adds {@link Intent#FLAG_ACTIVITY_NEW_TASK} to the intent - * before launching it. - */ - void send(Intent intent) { - if (applicationContext == null) { - Log.wtf(TAG, "Trying to send an intent before the applicationContext was initialized."); - return; - } - - Log.v(TAG, "Sending intent " + intent); - - if (activity != null) { - activity.startActivity(intent); - } else { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - applicationContext.startActivity(intent); - } - } - - /** - * Verifies the given intent and returns whether the application context class can resolve it. - * - *

This will fail to create and send the intent if {@code applicationContext} hasn't been set * - * at the time of calling. - * - *

This currently only supports resolving activities. - * - * @param intent Fully built intent. - * @see #buildIntent(String, Integer, String, Uri, Bundle, String, ComponentName, String) - * @return Whether the package manager found {@link android.content.pm.ResolveInfo} using its - * {@link PackageManager#resolveActivity(Intent, int)} method. - */ - boolean canResolveActivity(Intent intent) { - if (applicationContext == null) { - Log.wtf(TAG, "Trying to resolve an activity before the applicationContext was initialized."); - return false; - } - - final PackageManager packageManager = applicationContext.getPackageManager(); - - return packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null; - } - - /** Caches the given {@code activity} to use for {@link #send}. */ - void setActivity(@Nullable Activity activity) { - this.activity = activity; - } - - /** Caches the given {@code applicationContext} to use for {@link #send}. */ - void setApplicationContext(@Nullable Context applicationContext) { - this.applicationContext = applicationContext; - } - - /** - * Constructs a new intent with the data specified. - * - * @param action the Intent action, such as {@code ACTION_VIEW}. - * @param flags forwarded to {@link Intent#addFlags(int)} if non-null. - * @param category forwarded to {@link Intent#addCategory(String)} if non-null. - * @param data forwarded to {@link Intent#setData(Uri)} if non-null and 'type' parameter is null. - * If both 'data' and 'type' is non-null they're forwarded to {@link - * Intent#setDataAndType(Uri, String)} - * @param arguments forwarded to {@link Intent#putExtras(Bundle)} if non-null. - * @param packageName forwarded to {@link Intent#setPackage(String)} if non-null. This is forced - * to null if it can't be resolved. - * @param componentName forwarded to {@link Intent#setComponent(ComponentName)} if non-null. - * @param type forwarded to {@link Intent#setType(String)} if non-null and 'data' parameter is - * null. If both 'data' and 'type' is non-null they're forwarded to {@link - * Intent#setDataAndType(Uri, String)} - * @return Fully built intent. - */ - Intent buildIntent( - @Nullable String action, - @Nullable Integer flags, - @Nullable String category, - @Nullable Uri data, - @Nullable Bundle arguments, - @Nullable String packageName, - @Nullable ComponentName componentName, - @Nullable String type) { - if (applicationContext == null) { - Log.wtf(TAG, "Trying to build an intent before the applicationContext was initialized."); - return null; - } - - Intent intent = new Intent(); - - if (action != null) { - intent.setAction(action); - } - if (flags != null) { - intent.addFlags(flags); - } - if (!TextUtils.isEmpty(category)) { - intent.addCategory(category); - } - if (data != null && type == null) { - intent.setData(data); - } - if (type != null && data == null) { - intent.setType(type); - } - if (type != null && data != null) { - intent.setDataAndType(data, type); - } - if (arguments != null) { - intent.putExtras(arguments); - } - if (!TextUtils.isEmpty(packageName)) { - intent.setPackage(packageName); - if (componentName != null) { - intent.setComponent(componentName); - } - if (intent.resolveActivity(applicationContext.getPackageManager()) == null) { - Log.i(TAG, "Cannot resolve explicit intent - ignoring package"); - intent.setPackage(null); - } - } - - return intent; - } -} diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java deleted file mode 100644 index bcd843b64228..000000000000 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/MethodCallHandlerImpl.java +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.androidintent; - -import android.content.ComponentName; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -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 java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** Forwards incoming {@link MethodCall}s to {@link IntentSender#send}. */ -public final class MethodCallHandlerImpl implements MethodCallHandler { - private static final String TAG = "MethodCallHandlerImpl"; - private final IntentSender sender; - @Nullable private MethodChannel methodChannel; - - /** - * Uses the given {@code sender} for all incoming calls. - * - *

This assumes that the sender's context and activity state are managed elsewhere and - * correctly initialized before being sent here. - */ - MethodCallHandlerImpl(IntentSender sender) { - this.sender = sender; - } - - /** - * Registers this instance as a method call handler on the given {@code messenger}. - * - *

Stops any previously started and unstopped calls. - * - *

This should be cleaned with {@link #stopListening} once the messenger is disposed of. - */ - void startListening(BinaryMessenger messenger) { - if (methodChannel != null) { - Log.wtf(TAG, "Setting a method call handler before the last was disposed."); - stopListening(); - } - - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/android_intent"); - methodChannel.setMethodCallHandler(this); - } - - /** - * Clears this instance from listening to method calls. - * - *

Does nothing is {@link #startListening} hasn't been called, or if we're already stopped. - */ - void stopListening() { - if (methodChannel == null) { - Log.d(TAG, "Tried to stop listening when no methodChannel had been initialized."); - return; - } - - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - - /** - * Parses the incoming call and forwards it to the cached {@link IntentSender}. - * - *

Always calls {@code result#success}. - */ - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { - String action = convertAction((String) call.argument("action")); - Integer flags = call.argument("flags"); - String category = call.argument("category"); - String stringData = call.argument("data"); - Uri data = call.argument("data") != null ? Uri.parse(stringData) : null; - Map stringMap = call.argument("arguments"); - Bundle arguments = convertArguments(stringMap); - String packageName = call.argument("package"); - String component = call.argument("componentName"); - ComponentName componentName = null; - if (packageName != null - && component != null - && !TextUtils.isEmpty(packageName) - && !TextUtils.isEmpty(component)) { - componentName = new ComponentName(packageName, component); - } - String type = call.argument("type"); - - Intent intent = - sender.buildIntent( - action, flags, category, data, arguments, packageName, componentName, type); - - if ("launch".equalsIgnoreCase(call.method)) { - sender.send(intent); - - result.success(null); - } else if ("canResolveActivity".equalsIgnoreCase(call.method)) { - result.success(sender.canResolveActivity(intent)); - } else { - result.notImplemented(); - } - } - - private static String convertAction(String action) { - if (action == null) { - return null; - } - - switch (action) { - case "action_view": - return Intent.ACTION_VIEW; - case "action_voice": - return Intent.ACTION_VOICE_COMMAND; - case "settings": - return Settings.ACTION_SETTINGS; - case "action_location_source_settings": - return Settings.ACTION_LOCATION_SOURCE_SETTINGS; - case "action_application_details_settings": - return Settings.ACTION_APPLICATION_DETAILS_SETTINGS; - default: - return action; - } - } - - private static Bundle convertArguments(Map arguments) { - Bundle bundle = new Bundle(); - if (arguments == null) { - return bundle; - } - for (String key : arguments.keySet()) { - Object value = arguments.get(key); - ArrayList stringArrayList = isStringArrayList(value); - ArrayList integerArrayList = isIntegerArrayList(value); - Map stringMap = isStringKeyedMap(value); - if (value instanceof Integer) { - bundle.putInt(key, (Integer) value); - } else if (value instanceof String) { - bundle.putString(key, (String) value); - } else if (value instanceof Boolean) { - bundle.putBoolean(key, (Boolean) value); - } else if (value instanceof Double) { - bundle.putDouble(key, (Double) value); - } else if (value instanceof Long) { - bundle.putLong(key, (Long) value); - } else if (value instanceof byte[]) { - bundle.putByteArray(key, (byte[]) value); - } else if (value instanceof int[]) { - bundle.putIntArray(key, (int[]) value); - } else if (value instanceof long[]) { - bundle.putLongArray(key, (long[]) value); - } else if (value instanceof double[]) { - bundle.putDoubleArray(key, (double[]) value); - } else if (integerArrayList != null) { - bundle.putIntegerArrayList(key, integerArrayList); - } else if (stringArrayList != null) { - bundle.putStringArrayList(key, stringArrayList); - } else if (stringMap != null) { - bundle.putBundle(key, convertArguments(stringMap)); - } else { - throw new UnsupportedOperationException("Unsupported type " + value); - } - } - return bundle; - } - - private static ArrayList isIntegerArrayList(Object value) { - ArrayList integerArrayList = new ArrayList<>(); - if (!(value instanceof ArrayList)) { - return null; - } - ArrayList intList = (ArrayList) value; - for (Object o : intList) { - if (!(o instanceof Integer)) { - return null; - } else { - integerArrayList.add((Integer) o); - } - } - return integerArrayList; - } - - private static ArrayList isStringArrayList(Object value) { - ArrayList stringArrayList = new ArrayList<>(); - if (!(value instanceof ArrayList)) { - return null; - } - ArrayList stringList = (ArrayList) value; - for (Object o : stringList) { - if (!(o instanceof String)) { - return null; - } else { - stringArrayList.add((String) o); - } - } - return stringArrayList; - } - - private static Map isStringKeyedMap(Object value) { - Map stringMap = new HashMap<>(); - if (!(value instanceof Map)) { - return null; - } - Map mapValue = (Map) value; - for (Object key : mapValue.keySet()) { - if (!(key instanceof String)) { - return null; - } else { - Object o = mapValue.get(key); - if (o != null) { - stringMap.put((String) key, o); - } - } - } - return stringMap; - } -} diff --git a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java deleted file mode 100644 index 0ea03a0690f1..000000000000 --- a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java +++ /dev/null @@ -1,290 +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.androidintent; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.robolectric.Shadows.shadowOf; - -import android.app.Application; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import androidx.test.core.app.ApplicationProvider; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel.Result; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowPackageManager; - -@RunWith(RobolectricTestRunner.class) -public class MethodCallHandlerImplTest { - private static final String CHANNEL_NAME = "plugins.flutter.io/android_intent"; - private Context context; - private IntentSender sender; - private MethodCallHandlerImpl methodCallHandler; - - @Before - public void setUp() { - context = ApplicationProvider.getApplicationContext(); - sender = new IntentSender(null, null); - methodCallHandler = new MethodCallHandlerImpl(sender); - } - - @Test - public void startListening_registersChannel() { - BinaryMessenger messenger = mock(BinaryMessenger.class); - - methodCallHandler.startListening(messenger); - - verify(messenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - } - - @Test - public void startListening_unregistersExistingChannel() { - BinaryMessenger firstMessenger = mock(BinaryMessenger.class); - BinaryMessenger secondMessenger = mock(BinaryMessenger.class); - methodCallHandler.startListening(firstMessenger); - - methodCallHandler.startListening(secondMessenger); - - // Unregisters the first and then registers the second. - verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - verify(secondMessenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - } - - @Test - public void stopListening_unregistersExistingChannel() { - BinaryMessenger messenger = mock(BinaryMessenger.class); - methodCallHandler.startListening(messenger); - - methodCallHandler.stopListening(); - - verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - } - - @Test - public void stopListening_doesNothingWhenUnset() { - BinaryMessenger messenger = mock(BinaryMessenger.class); - - methodCallHandler.stopListening(); - - verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); - } - - @Test - public void onMethodCall_doesNothingWhenContextIsNull() { - Result result = mock(Result.class); - Map args = new HashMap<>(); - args.put("action", "foo"); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - // No matter what, should always succeed. - verify(result, times(1)).success(null); - assertNull(shadowOf((Application) context).getNextStartedActivity()); - } - - @Test - public void onMethodCall_setsAction() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertEquals("foo", intent.getAction()); - } - - @Test - public void onMethodCall_setsNewTaskFlagWithApplicationContext() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK, intent.getFlags()); - } - - @Test - public void onMethodCall_addsFlags() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - Integer requestFlags = Intent.FLAG_FROM_BACKGROUND; - args.put("flags", requestFlags); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertEquals(Intent.FLAG_ACTIVITY_NEW_TASK | requestFlags, intent.getFlags()); - } - - @Test - public void onMethodCall_addsCategory() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - String category = "bar"; - args.put("category", category); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertTrue(intent.getCategories().contains(category)); - } - - @Test - public void onMethodCall_setsData() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - Uri data = Uri.parse("http://flutter.dev"); - args.put("data", data.toString()); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertEquals(data, intent.getData()); - } - - @Test - public void onMethodCall_clearsInvalidPackageNames() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - args.put("packageName", "invalid"); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertNull(intent.getPackage()); - } - - @Test - public void onMethodCall_setsComponentName() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - ComponentName expectedComponent = - new ComponentName("io.flutter.plugins.androidintent", "MainActivity"); - args.put("action", "foo"); - args.put("package", expectedComponent.getPackageName()); - args.put("componentName", expectedComponent.getClassName()); - Result result = mock(Result.class); - ShadowPackageManager shadowPm = - shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()); - shadowPm.addActivityIfNotPresent(expectedComponent); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertNotNull(intent.getComponent()); - assertEquals("foo", intent.getAction()); - assertEquals("io.flutter.plugins.androidintent", intent.getPackage()); - assertEquals( - "io.flutter.plugins.androidintent/MainActivity", intent.getComponent().flattenToString()); - } - - @Test - public void onMethodCall_setsOnlyComponentName() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - ComponentName expectedComponent = - new ComponentName("io.flutter.plugins.androidintent", "MainActivity"); - args.put("package", expectedComponent.getPackageName()); - args.put("componentName", expectedComponent.getClassName()); - Result result = mock(Result.class); - ShadowPackageManager shadowPm = - shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()); - shadowPm.addActivityIfNotPresent(expectedComponent); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertNotNull(intent.getComponent()); - assertEquals("io.flutter.plugins.androidintent", intent.getPackage()); - assertEquals( - "io.flutter.plugins.androidintent/MainActivity", intent.getComponent().flattenToString()); - } - - @Test - public void onMethodCall_setsType() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - String type = "video/*"; - args.put("type", type); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertEquals(type, intent.getType()); - } - - @Test - public void onMethodCall_setsDataAndType() { - sender.setApplicationContext(context); - Map args = new HashMap<>(); - args.put("action", "foo"); - Uri data = Uri.parse("http://flutter.dev"); - args.put("data", data.toString()); - String type = "video/*"; - args.put("type", type); - Result result = mock(Result.class); - - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - - verify(result, times(1)).success(null); - Intent intent = shadowOf((Application) context).getNextStartedActivity(); - assertNotNull(intent); - assertEquals(type, intent.getType()); - assertEquals(data, intent.getData()); - } -} diff --git a/packages/android_intent/android_intent_android.iml b/packages/android_intent/android_intent_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/android_intent/android_intent_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/android_intent/example/README.md b/packages/android_intent/example/README.md deleted file mode 100644 index 460d46efe631..000000000000 --- a/packages/android_intent/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# android_intent_example - -Demonstrates how to use the android_intent plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/android_intent/example/android.iml b/packages/android_intent/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/android_intent/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/android_intent/example/android/app/build.gradle b/packages/android_intent/example/android/app/build.gradle deleted file mode 100644 index a309dc2e2d5c..000000000000 --- a/packages/android_intent/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.androidintentexample" - minSdkVersion 16 - 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 - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java deleted file mode 100644 index 358fc78bfcfd..000000000000 --- a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java +++ /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. - -package io.flutter.plugins.androidintentexample; - -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/android_intent/example/android/app/src/main/AndroidManifest.xml b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index e0aa7f84d7b9..000000000000 --- a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/packages/android_intent/example/android/gradle.properties b/packages/android_intent/example/android/gradle.properties deleted file mode 100644 index d2032bce8be6..000000000000 --- a/packages/android_intent/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableJetifier=true -android.useAndroidX=true -android.enableR8=true diff --git a/packages/android_intent/example/android_intent_example.iml b/packages/android_intent/example/android_intent_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/android_intent/example/android_intent_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/android_intent/example/android_intent_example_android.iml b/packages/android_intent/example/android_intent_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/android_intent/example/android_intent_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/android_intent/example/integration_test/android_intent_test.dart b/packages/android_intent/example/integration_test/android_intent_test.dart deleted file mode 100644 index 5ae86cba6a03..000000000000 --- a/packages/android_intent/example/integration_test/android_intent_test.dart +++ /dev/null @@ -1,75 +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 'dart:io'; - -import 'package:android_intent/android_intent.dart'; -import 'package:android_intent_example/main.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -/// This is a smoke test that verifies that the example app builds and loads. -/// Because this plugin works by launching Android platform UIs it's not -/// possible to meaningfully test it through its Dart interface currently. There -/// are more useful unit tests for the platform logic under android/src/test/. -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Embedding example app loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that the new embedding example app builds - if (Platform.isAndroid) { - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data.startsWith('Tap here'), - ), - findsNWidgets(2), - ); - } else { - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('This plugin only works with Android'), - ), - findsOneWidget, - ); - } - }); - - testWidgets('#launch throws when no Activity is found', - (WidgetTester tester) async { - // We can't test that any of this is really working, this is mostly just - // checking that the plugin API is registered. Only works on Android. - const AndroidIntent intent = - AndroidIntent(action: 'LAUNCH', package: 'foobar'); - await expectLater(() async => await intent.launch(), throwsA((Exception e) { - return e is PlatformException && - e.message.contains('No Activity found to handle Intent'); - })); - }, skip: !Platform.isAndroid); - - testWidgets('#canResolveActivity returns true when example Activity is found', - (WidgetTester tester) async { - AndroidIntent intent = AndroidIntent( - action: 'action_view', - package: 'io.flutter.plugins.androidintentexample', - componentName: 'io.flutter.embedding.android.FlutterActivity', - ); - await expectLater(() async => await intent.canResolveActivity(), isFalse); - }, skip: !Platform.isAndroid); - - testWidgets('#canResolveActivity returns false when no Activity is found', - (WidgetTester tester) async { - const AndroidIntent intent = - AndroidIntent(action: 'LAUNCH', package: 'foobar'); - await expectLater(() async => await intent.canResolveActivity(), isFalse); - }, skip: !Platform.isAndroid); -} diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart deleted file mode 100644 index c2276d080aa4..000000000000 --- a/packages/android_intent/example/lib/main.dart +++ /dev/null @@ -1,214 +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:android_intent/android_intent.dart'; -import 'package:android_intent/flag.dart'; -import 'package:flutter/material.dart'; -import 'package:platform/platform.dart'; - -void main() { - runApp(MyApp()); -} - -/// A sample app for launching intents. -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(), - routes: { - ExplicitIntentsWidget.routeName: (BuildContext context) => - const ExplicitIntentsWidget() - }, - ); - } -} - -/// Holds the different intent widgets. -class MyHomePage extends StatelessWidget { - void _createAlarm() { - final AndroidIntent intent = const AndroidIntent( - action: 'android.intent.action.SET_ALARM', - arguments: { - 'android.intent.extra.alarm.DAYS': [2, 3, 4, 5, 6], - 'android.intent.extra.alarm.HOUR': 21, - 'android.intent.extra.alarm.MINUTES': 30, - 'android.intent.extra.alarm.SKIP_UI': true, - 'android.intent.extra.alarm.MESSAGE': 'Create a Flutter app', - }, - ); - intent.launch(); - } - - void _openExplicitIntentsView(BuildContext context) { - Navigator.of(context).pushNamed(ExplicitIntentsWidget.routeName); - } - - @override - Widget build(BuildContext context) { - Widget body; - if (const LocalPlatform().isAndroid) { - body = Padding( - padding: const EdgeInsets.symmetric(vertical: 15.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - child: const Text( - 'Tap here to set an alarm\non weekdays at 9:30pm.'), - onPressed: _createAlarm, - ), - ElevatedButton( - child: const Text('Tap here to test explicit intents.'), - onPressed: () => _openExplicitIntentsView(context)), - ], - ), - ); - } else { - body = const Text('This plugin only works with Android'); - } - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center(child: body), - ); - } -} - -/// Launches intents to specific Android activities. -class ExplicitIntentsWidget extends StatelessWidget { - const ExplicitIntentsWidget(); // ignore: public_member_api_docs - - // ignore: public_member_api_docs - static const String routeName = "/explicitIntents"; - - void _openGoogleMapsStreetView() { - final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull('google.streetview:cbll=46.414382,10.013988'), - package: 'com.google.android.apps.maps'); - intent.launch(); - } - - void _displayMapInGoogleMaps({int zoomLevel = 12}) { - final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull('geo:37.7749,-122.4194?z=$zoomLevel'), - package: 'com.google.android.apps.maps'); - intent.launch(); - } - - void _launchTurnByTurnNavigationInGoogleMaps() { - final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull( - 'google.navigation:q=Taronga+Zoo,+Sydney+Australia&avoid=tf'), - package: 'com.google.android.apps.maps'); - intent.launch(); - } - - void _openLinkInGoogleChrome() { - final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull('https://flutter.io'), - package: 'com.android.chrome'); - intent.launch(); - } - - void _startActivityInNewTask() { - final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull('https://flutter.io'), - flags: [Flag.FLAG_ACTIVITY_NEW_TASK], - ); - intent.launch(); - } - - void _testExplicitIntentFallback() { - final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull('https://flutter.io'), - package: 'com.android.chrome.implicit.fallback'); - intent.launch(); - } - - void _openLocationSettingsConfiguration() { - final AndroidIntent intent = const AndroidIntent( - action: 'action_location_source_settings', - ); - intent.launch(); - } - - void _openApplicationDetails() { - final AndroidIntent intent = const AndroidIntent( - action: 'action_application_details_settings', - data: 'package:io.flutter.plugins.androidintentexample', - ); - intent.launch(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Test explicit intents'), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 15.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - child: const Text( - 'Tap here to display panorama\nimagery in Google Street View.'), - onPressed: _openGoogleMapsStreetView, - ), - ElevatedButton( - child: const Text('Tap here to display\na map in Google Maps.'), - onPressed: _displayMapInGoogleMaps, - ), - ElevatedButton( - child: const Text( - 'Tap here to launch turn-by-turn\nnavigation in Google Maps.'), - onPressed: _launchTurnByTurnNavigationInGoogleMaps, - ), - ElevatedButton( - child: const Text('Tap here to open link in Google Chrome.'), - onPressed: _openLinkInGoogleChrome, - ), - ElevatedButton( - child: const Text('Tap here to start activity in new task.'), - onPressed: _startActivityInNewTask, - ), - ElevatedButton( - child: const Text( - 'Tap here to test explicit intent fallback to implicit.'), - onPressed: _testExplicitIntentFallback, - ), - ElevatedButton( - child: const Text( - 'Tap here to open Location Settings Configuration', - ), - onPressed: _openLocationSettingsConfiguration, - ), - ElevatedButton( - child: const Text( - 'Tap here to open Application Details', - ), - onPressed: _openApplicationDetails, - ) - ], - ), - ), - ), - ); - } -} diff --git a/packages/android_intent/example/pubspec.yaml b/packages/android_intent/example/pubspec.yaml deleted file mode 100644 index 42e59930ce64..000000000000 --- a/packages/android_intent/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: android_intent_example -description: Demonstrates how to use the android_intent plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - android_intent: - # When depending on this package from a real application you should use: - # android_intent: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - integration_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.10.0 - -# The following section is specific to Flutter. -flutter: - uses-material-design: true diff --git a/packages/android_intent/example/test_driver/integration_test.dart b/packages/android_intent/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/android_intent/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart deleted file mode 100644 index 80208833c6be..000000000000 --- a/packages/android_intent/lib/android_intent.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 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; -import 'package:platform/platform.dart'; - -const String _kChannelName = 'plugins.flutter.io/android_intent'; - -/// Flutter plugin for launching arbitrary Android Intents. -/// -/// See [the official Android -/// documentation](https://developer.android.com/reference/android/content/Intent.html) -/// for more information on how to use Intents. -class AndroidIntent { - /// Builds an Android intent with the following parameters - /// [action] refers to the action parameter of the intent. - /// [flags] is the list of int that will be converted to native flags. - /// [category] refers to the category of the intent, can be null. - /// [data] refers to the string format of the URI that will be passed to - /// intent. - /// [arguments] is the map that will be converted into an extras bundle and - /// passed to the intent. - /// [package] refers to the package parameter of the intent, can be null. - /// [componentName] refers to the component name of the intent, can be null. - /// If not null, then [package] but also be provided. - /// [type] refers to the type of the intent, can be null. - const AndroidIntent({ - this.action, - this.flags, - this.category, - this.data, - this.arguments, - this.package, - this.componentName, - Platform? platform, - this.type, - }) : assert(action != null || componentName != null, - 'action or component (or both) must be specified'), - _channel = const MethodChannel(_kChannelName), - _platform = platform ?? const LocalPlatform(); - - /// This constructor is only exposed for unit testing. Do not rely on this in - /// app code, it may break without warning. - @visibleForTesting - AndroidIntent.private({ - required Platform platform, - required MethodChannel channel, - this.action, - this.flags, - this.category, - this.data, - this.arguments, - this.package, - this.componentName, - this.type, - }) : assert(action != null || componentName != null, - 'action or component (or both) must be specified'), - _channel = channel, - _platform = platform; - - /// This is the general verb that the intent should attempt to do. This - /// includes constants like `ACTION_VIEW`. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String? action; - - /// Constants that can be set on an intent to tweak how it is finally handled. - /// Some of the constants are mirrored to Dart via [Flag]. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#setFlags(int). - final List? flags; - - /// An optional additional constant qualifying the given [action]. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String? category; - - /// The Uri that the [action] is pointed towards. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String? data; - - /// The equivalent of `extras`, a generic `Bundle` of data that the Intent can - /// carry. This is a slot for extraneous data that the listener may use. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final Map? arguments; - - /// Sets the [data] to only resolve within this given package. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#setPackage(java.lang.String). - final String? package; - - /// Set the exact `ComponentName` that should handle the intent. If this is - /// set [package] should also be non-null. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#setComponent(android.content.ComponentName). - final String? componentName; - final MethodChannel _channel; - final Platform _platform; - - /// Set an explicit MIME data type. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String? type; - - bool _isPowerOfTwo(int x) { - /* First x in the below expression is for the case when x is 0 */ - return x != 0 && ((x & (x - 1)) == 0); - } - - /// This method is just visible for unit testing and should not be relied on. - /// Its method signature may change at any time. - @visibleForTesting - int convertFlags(List flags) { - int finalValue = 0; - for (int i = 0; i < flags.length; i++) { - if (!_isPowerOfTwo(flags[i])) { - throw ArgumentError.value(flags[i], 'flag\'s value must be power of 2'); - } - finalValue |= flags[i]; - } - return finalValue; - } - - /// Launch the intent. - /// - /// This works only on Android platforms. - Future launch() async { - if (!_platform.isAndroid) { - return; - } - - await _channel.invokeMethod('launch', _buildArguments()); - } - - /// Check whether the intent can be resolved to an activity. - /// - /// This works only on Android platforms. - Future canResolveActivity() async { - if (!_platform.isAndroid) { - return false; - } - - final result = await _channel.invokeMethod( - 'canResolveActivity', - _buildArguments(), - ); - return result!; - } - - /// Constructs the map of arguments which is passed to the plugin. - Map _buildArguments() { - return { - if (action != null) 'action': action, - if (flags != null) 'flags': convertFlags(flags!), - if (category != null) 'category': category, - if (data != null) 'data': data, - if (arguments != null) 'arguments': arguments, - if (package != null) ...{ - 'package': package, - if (componentName != null) 'componentName': componentName, - }, - if (type != null) 'type': type, - }; - } -} diff --git a/packages/android_intent/lib/flag.dart b/packages/android_intent/lib/flag.dart deleted file mode 100644 index 771a89ff83a7..000000000000 --- a/packages/android_intent/lib/flag.dart +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// Special flags that can be set on an intent to control how it is handled. -/// -/// See -/// https://developer.android.com/reference/android/content/Intent.html#setFlags(int) -/// for the official documentation on Intent flags. The constants here mirror -/// the existing [android.content.Intent] ones. -class Flag { - /// Specifies how an activity should be launched. Generally set by the system - /// in conjunction with SINGLE_TASK. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_BROUGHT_TO_FRONT. - static const int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; - - /// Causes any existing tasks associated with the activity to be cleared. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TASK - static const int FLAG_ACTIVITY_CLEAR_TASK = 32768; - - /// Closes any activities on top of this activity and brings it to the front, - /// if it's currently running. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_TOP - static const int FLAG_ACTIVITY_CLEAR_TOP = 67108864; - - /// @deprecated Use [FLAG_ACTIVITY_NEW_DOCUMENT] instead when on API 21 or above. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET - @deprecated - static const int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; - - /// Keeps the activity from being listed with other recently launched - /// activities. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - static const int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; - - /// Forwards the result from this activity to the existing one. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_FORWARD_RESULT - static const int FLAG_ACTIVITY_FORWARD_RESULT = 33554432; - - /// Generally set by the system if the activity is being launched from - /// history. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY - static const int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; - - /// Used in split-screen mode to set the launched activity adjacent to the - /// launcher. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_LAUNCH_ADJACENT - static const int FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; - - /// Used in split-screen mode to set the launched activity adjacent to the - /// launcher. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MATCH_EXTERNAL - static const int FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; - - /// Creates and launches the activity into a new task. Should always be - /// combined with [FLAG_ACTIVITY_NEW_DOCUMENT] or [FLAG_ACTIVITY_NEW_TASK]. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_MULTIPLE_TASK. - static const int FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; - - /// Opens a document into a new task rooted in this activity. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_DOCUMENT. - static const int FLAG_ACTIVITY_NEW_DOCUMENT = 524288; - - /// The launched activity starts a new task on the activity stack. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NEW_TASK. - static const int FLAG_ACTIVITY_NEW_TASK = 268435456; - - /// Prevents the system from playing an activity transition animation when - /// launching this. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_ANIMATION. - static const int FLAG_ACTIVITY_NO_ANIMATION = 65536; - - /// Does not keep the launched activity in history. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_HISTORY. - static const int FLAG_ACTIVITY_NO_HISTORY = 1073741824; - - /// Prevents a typical callback from occuring when the activity is paused. - /// - /// https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_NO_USER_ACTION - static const int FLAG_ACTIVITY_NO_USER_ACTION = 262144; - - /// Uses the previous activity as top when applicable. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_PREVIOUS_IS_TOP. - static const int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; - - /// Brings any already instances of this activity to the front. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_REORDER_TO_FRONT. - static const int FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; - - /// Launches the activity in a way that resets the task in some cases. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_RESET_TASK_IF_NEEDED. - static const int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; - - /// Keeps an entry in recent tasks. Used with [FLAG_ACTIVITY_NEW_DOCUMENT]. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_RETAIN_IN_RECENTS. - static const int FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; - - /// Will not re-launch the activity if it is already at the top of the history - /// stack. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_SINGLE_TOP. - static const int FLAG_ACTIVITY_SINGLE_TOP = 536870912; - - /// Places the activity on top of the home task. Must be used with - /// [FLAG_ACTIVITY_NEW_TASK]. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_ACTIVITY_TASK_ON_HOME. - static const int FLAG_ACTIVITY_TASK_ON_HOME = 16384; - - /// Prints debug logs while the intent is resolving. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_DEBUG_LOG_RESOLUTION. - static const int FLAG_DEBUG_LOG_RESOLUTION = 8; - - /// Does not match to any stopped components. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_EXCLUDE_STOPPED_PACKAGES. - static const int FLAG_EXCLUDE_STOPPED_PACKAGES = 16; - - /// Can be set by the caller to flag the intent as not being launched directly - /// by the user. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_FROM_BACKGROUND. - static const int FLAG_FROM_BACKGROUND = 4; - - /// Will persist the URI permision across device reboots. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_PERSISTABLE_URI_PERMISSION. - static const int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; - - /// Applies the URI permission grant based on prefix matching. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_PREFIX_URI_PERMISSION. - static const int FLAG_GRANT_PREFIX_URI_PERMISSION = 128; - - /// Grants the intent listener permission to read extra data from the Intent's - /// URI. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_READ_URI_PERMISSION. - static const int FLAG_GRANT_READ_URI_PERMISSION = 1; - - /// Grants the intent listener permission to write extra data from the - /// Intent's URI. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_GRANT_WRITE_URI_PERMISSION. - static const int FLAG_GRANT_WRITE_URI_PERMISSION = 2; - - /// Always matches stopped components. This is the default behavior. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_INCLUDE_STOPPED_PACKAGES. - static const int FLAG_INCLUDE_STOPPED_PACKAGES = 32; - - /// Allows the listener to run at a high priority. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_FOREGROUND. - static const int FLAG_RECEIVER_FOREGROUND = 268435456; - - /// Doesn't allow listeners to cancel the broadcast. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_NO_ABORT. - static const int FLAG_RECEIVER_NO_ABORT = 134217728; - - /// Only allows registered receivers to listen for the intent. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_REGISTERED_ONLY. - static const int FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; - - /// Will drop any pending broadcasts of this intent in favor of the newest - /// one. - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_REPLACE_PENDING. - static const int FLAG_RECEIVER_REPLACE_PENDING = 536870912; - - /// Instant Apps will be able to listen for the intent (not the default - /// behavior). - /// - /// See https://developer.android.com/reference/android/content/Intent.html#FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS. - static const int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; -} diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml deleted file mode 100644 index 793f82d4762d..000000000000 --- a/packages/android_intent/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: android_intent -description: Flutter plugin for launching Android Intents. Not supported on iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/android_intent -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+android_intent%22 -version: 2.0.2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.androidintent - pluginClass: AndroidIntentPlugin - -dependencies: - flutter: - sdk: flutter - platform: ^3.0.0 - meta: ^1.3.0 -dev_dependencies: - test: ^1.16.3 - mockito: ^5.0.0 - flutter_test: - sdk: flutter - pedantic: ^1.10.0 - build_runner: ^1.11.1 diff --git a/packages/android_intent/test/android_intent_test.dart b/packages/android_intent/test/android_intent_test.dart deleted file mode 100644 index 00bcc7664908..000000000000 --- a/packages/android_intent/test/android_intent_test.dart +++ /dev/null @@ -1,184 +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:android_intent/flag.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:android_intent/android_intent.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; - -import 'android_intent_test.mocks.dart'; - -@GenerateMocks([MethodChannel]) -void main() { - late AndroidIntent androidIntent; - late MockMethodChannel mockChannel; - - setUp(() { - mockChannel = MockMethodChannel(); - when(mockChannel.invokeMethod('canResolveActivity', any)) - .thenAnswer((realInvocation) async => true); - when(mockChannel.invokeMethod('launch', any)) - .thenAnswer((realInvocation) async => {}); - }); - - group('AndroidIntent', () { - test('raises error if neither an action nor a component is provided', () { - try { - androidIntent = AndroidIntent(data: 'https://flutter.io'); - fail('should raise an AssertionError'); - } on AssertionError catch (e) { - expect(e.message, 'action or component (or both) must be specified'); - } catch (e) { - fail('should raise an AssertionError'); - } - }); - - group('launch', () { - test('pass right params', () async { - androidIntent = AndroidIntent.private( - action: 'action_view', - data: Uri.encodeFull('https://flutter.io'), - flags: [Flag.FLAG_ACTIVITY_NEW_TASK], - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'android'), - type: 'video/*'); - await androidIntent.launch(); - verify(mockChannel.invokeMethod('launch', { - 'action': 'action_view', - 'data': Uri.encodeFull('https://flutter.io'), - 'flags': - androidIntent.convertFlags([Flag.FLAG_ACTIVITY_NEW_TASK]), - 'type': 'video/*', - })); - }); - - test('can send Intent with an action and no component', () async { - androidIntent = AndroidIntent.private( - action: 'action_view', - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'android'), - ); - await androidIntent.launch(); - verify(mockChannel.invokeMethod('launch', { - 'action': 'action_view', - })); - }); - - test('can send Intent with a component and no action', () async { - androidIntent = AndroidIntent.private( - package: 'packageName', - componentName: 'componentName', - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'android'), - ); - await androidIntent.launch(); - verify(mockChannel.invokeMethod('launch', { - 'package': 'packageName', - 'componentName': 'componentName', - })); - }); - - test('call in ios platform', () async { - androidIntent = AndroidIntent.private( - action: 'action_view', - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'ios')); - await androidIntent.launch(); - verifyZeroInteractions(mockChannel); - }); - }); - - group('canResolveActivity', () { - test('pass right params', () async { - androidIntent = AndroidIntent.private( - action: 'action_view', - data: Uri.encodeFull('https://flutter.io'), - flags: [Flag.FLAG_ACTIVITY_NEW_TASK], - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'android'), - type: 'video/*'); - await androidIntent.canResolveActivity(); - verify(mockChannel - .invokeMethod('canResolveActivity', { - 'action': 'action_view', - 'data': Uri.encodeFull('https://flutter.io'), - 'flags': - androidIntent.convertFlags([Flag.FLAG_ACTIVITY_NEW_TASK]), - 'type': 'video/*', - })); - }); - - test('can send Intent with an action and no component', () async { - androidIntent = AndroidIntent.private( - action: 'action_view', - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'android'), - ); - await androidIntent.canResolveActivity(); - verify(mockChannel - .invokeMethod('canResolveActivity', { - 'action': 'action_view', - })); - }); - - test('can send Intent with a component and no action', () async { - androidIntent = AndroidIntent.private( - package: 'packageName', - componentName: 'componentName', - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'android'), - ); - await androidIntent.canResolveActivity(); - verify(mockChannel - .invokeMethod('canResolveActivity', { - 'package': 'packageName', - 'componentName': 'componentName', - })); - }); - - test('call in ios platform', () async { - androidIntent = AndroidIntent.private( - action: 'action_view', - channel: mockChannel, - platform: FakePlatform(operatingSystem: 'ios')); - await androidIntent.canResolveActivity(); - verifyZeroInteractions(mockChannel); - }); - }); - }); - - group('convertFlags ', () { - androidIntent = const AndroidIntent( - action: 'action_view', - ); - test('add filled flag list', () async { - final List flags = []; - flags.add(Flag.FLAG_ACTIVITY_NEW_TASK); - flags.add(Flag.FLAG_ACTIVITY_NEW_DOCUMENT); - expect( - androidIntent.convertFlags(flags), - 268959744, - ); - }); - test('add flags whose values are not power of 2', () async { - final List flags = []; - flags.add(100); - flags.add(10); - expect( - () => androidIntent.convertFlags(flags), - throwsArgumentError, - ); - }); - test('add empty flag list', () async { - final List flags = []; - expect( - androidIntent.convertFlags(flags), - 0, - ); - }); - }); -} diff --git a/packages/android_intent/test/android_intent_test.mocks.dart b/packages/android_intent/test/android_intent_test.mocks.dart deleted file mode 100644 index fed1624ad069..000000000000 --- a/packages/android_intent/test/android_intent_test.mocks.dart +++ /dev/null @@ -1,64 +0,0 @@ -// Mocks generated by Mockito 5.0.0 from annotations -// in android_intent/test/android_intent_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; - -import 'package:flutter/src/services/binary_messenger.dart' as _i3; -import 'package:flutter/src/services/message_codec.dart' as _i2; -import 'package:flutter/src/services/platform_channel.dart' as _i4; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: comment_references -// ignore_for_file: unnecessary_parenthesis - -class _FakeMethodCodec extends _i1.Fake implements _i2.MethodCodec {} - -class _FakeBinaryMessenger extends _i1.Fake implements _i3.BinaryMessenger {} - -/// A class which mocks [MethodChannel]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { - MockMethodChannel() { - _i1.throwOnMissingStub(this); - } - - @override - String get name => - (super.noSuchMethod(Invocation.getter(#name), returnValue: '') as String); - @override - _i2.MethodCodec get codec => (super.noSuchMethod(Invocation.getter(#codec), - returnValue: _FakeMethodCodec()) as _i2.MethodCodec); - @override - _i3.BinaryMessenger get binaryMessenger => - (super.noSuchMethod(Invocation.getter(#binaryMessenger), - returnValue: _FakeBinaryMessenger()) as _i3.BinaryMessenger); - @override - _i5.Future invokeMethod(String? method, [dynamic arguments]) => - (super.noSuchMethod(Invocation.method(#invokeMethod, [method, arguments]), - returnValue: Future.value(null)) as _i5.Future); - @override - _i5.Future?> invokeListMethod(String? method, - [dynamic arguments]) => - (super.noSuchMethod( - Invocation.method(#invokeListMethod, [method, arguments]), - returnValue: Future.value([])) as _i5.Future?>); - @override - _i5.Future?> invokeMapMethod(String? method, - [dynamic arguments]) => - (super.noSuchMethod( - Invocation.method(#invokeMapMethod, [method, arguments]), - returnValue: Future.value({})) as _i5.Future?>); - @override - bool checkMethodCallHandler( - _i5.Future Function(_i2.MethodCall)? handler) => - (super.noSuchMethod(Invocation.method(#checkMethodCallHandler, [handler]), - returnValue: false) as bool); - @override - bool checkMockMethodCallHandler( - _i5.Future Function(_i2.MethodCall)? handler) => - (super.noSuchMethod( - Invocation.method(#checkMockMethodCallHandler, [handler]), - returnValue: false) as bool); -} diff --git a/packages/battery/analysis_options.yaml b/packages/battery/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/battery/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md deleted file mode 100644 index ddc912d2ba2a..000000000000 --- a/packages/battery/battery/CHANGELOG.md +++ /dev/null @@ -1,199 +0,0 @@ -## NEXT - -* Remove references to the Android v1 embedding. -* Updated Android lint settings. - -## 2.0.3 - -* Update README to point to Plus Plugins version. - -## 2.0.2 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.11 - -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. - -## 1.0.10 - -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) - -## 1.0.9 - -* Update Flutter SDK constraint. - -## 1.0.8 - -* Update Dart SDK constraint in example. - -## 1.0.7 - -* Update android compileSdkVersion to 29. - -## 1.0.6 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 1.0.5 - -* Ported to use platform interface. - -## 1.0.4+1 - -* Moved everything from battery to battery/battery - -## 1.0.4 - -* Updated README.md. - -## 1.0.3 - -* Update package:e2e to use package:integration_test - - -## 1.0.2 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 1.0.1 - -* Update lower bound of dart dependency to 2.1.0. - -## 1.0.0 - -* Bump the package version to 1.0.0 following ecosystem pre-migration (https://github.com/amirh/bump_to_1.0/projects/1). - -## 0.3.1+10 - -* Update minimum Flutter version to 1.12.13+hotfix.5 -* Fix CocoaPods podspec lint warnings. - -## 0.3.1+9 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.3.1+8 - -* Make the pedantic dev_dependency explicit. - -## 0.3.1+7 - -* Clean up various Android workarounds no longer needed after framework v1.12. - -## 0.3.1+6 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.3.1+5 - -* Fix pedantic linter errors. - -## 0.3.1+4 - -* Update and migrate iOS example project. - -## 0.3.1+3 - -* Remove AndroidX warning. - -## 0.3.1+2 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.3.1+1 - -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. - -## 0.3.1 - -* Support the v2 Android embedder. - -## 0.3.0+6 - -* Define clang module for iOS. - -## 0.3.0+5 - -* Fix Gradle version. - -## 0.3.0+4 - -* Update Dart code to conform to current Dart formatter. - -## 0.3.0+3 - -* Fix `batteryLevel` usage example in README - -## 0.3.0+2 - -* Bump the minimum Flutter version to 1.2.0. -* Add template type parameter to `invokeMethod` calls. - -## 0.3.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.3.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.2.3 - -* Updated mockito dependency to 3.0.0 to get Dart 2 support. -* Update test package dependency to 1.3.0, and fixed tests to match. - -## 0.2.2 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.1 - -* Fixed Dart 2 type error. -* Removed use of deprecated parameter in example. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.1 - -* Fixed warnings from the Dart 2.0 analyzer. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.2 - -* Add FLT prefix to iOS types. - -## 0.0.1+1 - -* Updated README - -## 0.0.1 - -* Initial release diff --git a/packages/battery/battery/README.md b/packages/battery/battery/README.md deleted file mode 100644 index 5337978b4d79..000000000000 --- a/packages/battery/battery/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Battery - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`battery_plus`](https://pub.dev/packages/battery_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -[![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dev/packages/battery) - -A Flutter plugin to access various information about the battery of the device the app is running on. - -## Usage -To use this plugin, add `battery` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). - -### Example - -``` dart -// Import package -import 'package:battery/battery.dart'; - -// Instantiate it -var _battery = Battery(); - -// Access current battery level -print(await _battery.batteryLevel); - -// Be informed when the state (full, charging, discharging) changes -_battery.onBatteryStateChanged.listen((BatteryState state) { - // Do something with new state -}); -``` diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle deleted file mode 100644 index 14f503813f7e..000000000000 --- a/packages/battery/battery/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.battery' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 4e974715fd7b..000000000000 --- a/packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/battery/battery/android/settings.gradle b/packages/battery/battery/android/settings.gradle deleted file mode 100644 index 14e52068a5ec..000000000000 --- a/packages/battery/battery/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'battery' diff --git a/packages/battery/battery/android/src/main/AndroidManifest.xml b/packages/battery/battery/android/src/main/AndroidManifest.xml deleted file mode 100644 index 480b04644e3a..000000000000 --- a/packages/battery/battery/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/battery/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java b/packages/battery/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java deleted file mode 100644 index 7f2e1efbeede..000000000000 --- a/packages/battery/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java +++ /dev/null @@ -1,131 +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.battery; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.BatteryManager; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.EventChannel.EventSink; -import io.flutter.plugin.common.EventChannel.StreamHandler; -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; - -/** BatteryPlugin */ -public class BatteryPlugin implements MethodCallHandler, StreamHandler, FlutterPlugin { - - private Context applicationContext; - private BroadcastReceiver chargingStateChangeReceiver; - private MethodChannel methodChannel; - private EventChannel eventChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - final BatteryPlugin instance = new BatteryPlugin(); - instance.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { - this.applicationContext = applicationContext; - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/battery"); - eventChannel = new EventChannel(messenger, "plugins.flutter.io/charging"); - eventChannel.setStreamHandler(this); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - applicationContext = null; - methodChannel.setMethodCallHandler(null); - methodChannel = null; - eventChannel.setStreamHandler(null); - eventChannel = null; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - if (call.method.equals("getBatteryLevel")) { - int batteryLevel = getBatteryLevel(); - - if (batteryLevel != -1) { - result.success(batteryLevel); - } else { - result.error("UNAVAILABLE", "Battery level not available.", null); - } - } else { - result.notImplemented(); - } - } - - @Override - public void onListen(Object arguments, EventSink events) { - chargingStateChangeReceiver = createChargingStateChangeReceiver(events); - applicationContext.registerReceiver( - chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - } - - @Override - public void onCancel(Object arguments) { - applicationContext.unregisterReceiver(chargingStateChangeReceiver); - chargingStateChangeReceiver = null; - } - - private int getBatteryLevel() { - int batteryLevel = -1; - if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - BatteryManager batteryManager = - (BatteryManager) applicationContext.getSystemService(applicationContext.BATTERY_SERVICE); - batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); - } else { - Intent intent = - new ContextWrapper(applicationContext) - .registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - batteryLevel = - (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) - / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); - } - - return batteryLevel; - } - - private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) { - return new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - - switch (status) { - case BatteryManager.BATTERY_STATUS_CHARGING: - events.success("charging"); - break; - case BatteryManager.BATTERY_STATUS_FULL: - events.success("full"); - break; - case BatteryManager.BATTERY_STATUS_DISCHARGING: - events.success("discharging"); - break; - default: - events.error("UNAVAILABLE", "Charging status unavailable", null); - break; - } - } - }; - } -} diff --git a/packages/battery/battery/example/README.md b/packages/battery/battery/example/README.md deleted file mode 100644 index ac3fc4d8a470..000000000000 --- a/packages/battery/battery/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# battery_example - -Demonstrates how to use the battery plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/battery/battery/example/android/app/build.gradle b/packages/battery/battery/example/android/app/build.gradle deleted file mode 100644 index 4fcd6ba0049e..000000000000 --- a/packages/battery/battery/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.batteryexample" - minSdkVersion 16 - 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 - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java deleted file mode 100644 index 5068d043bdfc..000000000000 --- a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java +++ /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. - -package io.flutter.plugins.batteryexample; - -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/battery/battery/example/android/app/src/main/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 11feb41de96a..000000000000 --- a/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/battery/battery/example/integration_test/battery_test.dart b/packages/battery/battery/example/integration_test/battery_test.dart deleted file mode 100644 index eced27e5a1cd..000000000000 --- a/packages/battery/battery/example/integration_test/battery_test.dart +++ /dev/null @@ -1,31 +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:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:battery/battery.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can get battery level', (WidgetTester tester) async { - final Battery battery = Battery(); - int batteryLevel; - try { - batteryLevel = await battery.batteryLevel; - } on PlatformException catch (e) { - // The "UNAVAIBLE" error just means that the system reported the battery - // level as unknown (e.g., the test is running on simulator); it still - // indicates that the plugin itself is working as expected, so consider it - // as passing. - if (e.code == 'UNAVAILABLE') { - batteryLevel = 1; - } - } - expect(batteryLevel, isNotNull); - }); -} diff --git a/packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist b/packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj b/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index f994b369afe9..000000000000 --- a/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,460 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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 */; }; - CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E2CD29898079A0E658445A5 /* libPods-Runner.a */; }; -/* 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 = ""; }; - 1E2CD29898079A0E658445A5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 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 = ""; }; - BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1C99224A167BC35DA0CD0913 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 1E2CD29898079A0E658445A5 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 571753FC2D526E56A295E627 /* Pods */ = { - isa = PBXGroup; - children = ( - 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */, - BF850F5DC44F7AE2B245B994 /* Pods-Runner.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 = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 571753FC2D526E56A295E627 /* Pods */, - 1C99224A167BC35DA0CD0913 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - C2BE46BB51B1181F0FD17925 /* [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 = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" 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"; - }; - C2BE46BB51B1181F0FD17925 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.batteryExample; - 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.batteryExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/battery/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/battery/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/battery/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/battery/battery/example/ios/Runner/Info.plist b/packages/battery/battery/example/ios/Runner/Info.plist deleted file mode 100644 index 1c5cdde068b9..000000000000 --- a/packages/battery/battery/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - battery_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/battery/battery/example/ios/Runner/main.m b/packages/battery/battery/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/battery/battery/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/battery/battery/example/lib/main.dart b/packages/battery/battery/example/lib/main.dart deleted file mode 100644 index b139d0d8e4be..000000000000 --- a/packages/battery/battery/example/lib/main.dart +++ /dev/null @@ -1,95 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:battery/battery.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final Battery _battery = Battery(); - - BatteryState? _batteryState; - late StreamSubscription _batteryStateSubscription; - - @override - void initState() { - super.initState(); - _batteryStateSubscription = - _battery.onBatteryStateChanged.listen((BatteryState state) { - setState(() { - _batteryState = state; - }); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('$_batteryState'), - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.battery_unknown), - onPressed: () async { - final int batteryLevel = await _battery.batteryLevel; - // ignore: unawaited_futures - showDialog( - context: context, - builder: (_) => AlertDialog( - content: Text('Battery: $batteryLevel%'), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); - }, - ), - ); - } - - @override - void dispose() { - super.dispose(); - if (_batteryStateSubscription != null) { - _batteryStateSubscription.cancel(); - } - } -} diff --git a/packages/battery/battery/example/pubspec.yaml b/packages/battery/battery/example/pubspec.yaml deleted file mode 100644 index e33dda9b86a3..000000000000 --- a/packages/battery/battery/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: battery_example -description: Demonstrates how to use the battery plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - battery: - # When depending on this package from a real application you should use: - # battery: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/battery/battery/example/test_driver/integration_test.dart b/packages/battery/battery/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/battery/battery/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h deleted file mode 100644 index fd6a3e964d83..000000000000 --- a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTBatteryPlugin : NSObject -@end diff --git a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m deleted file mode 100644 index 558d395bb9c0..000000000000 --- a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m +++ /dev/null @@ -1,96 +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 "FLTBatteryPlugin.h" - -@interface FLTBatteryPlugin () -@end - -@implementation FLTBatteryPlugin { - FlutterEventSink _eventSink; -} - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTBatteryPlugin* instance = [[FLTBatteryPlugin alloc] init]; - - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/battery" - binaryMessenger:[registrar messenger]]; - - [registrar addMethodCallDelegate:instance channel:channel]; - FlutterEventChannel* chargingChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/charging" - binaryMessenger:[registrar messenger]]; - [chargingChannel setStreamHandler:instance]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([@"getBatteryLevel" isEqualToString:call.method]) { - int batteryLevel = [self getBatteryLevel]; - if (batteryLevel == -1) { - result([FlutterError errorWithCode:@"UNAVAILABLE" - message:@"Battery info unavailable" - details:nil]); - } else { - result(@(batteryLevel)); - } - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)onBatteryStateDidChange:(NSNotification*)notification { - [self sendBatteryStateEvent]; -} - -- (void)sendBatteryStateEvent { - if (!_eventSink) return; - UIDeviceBatteryState state = [[UIDevice currentDevice] batteryState]; - switch (state) { - case UIDeviceBatteryStateFull: - _eventSink(@"full"); - case UIDeviceBatteryStateCharging: - _eventSink(@"charging"); - break; - case UIDeviceBatteryStateUnplugged: - _eventSink(@"discharging"); - break; - default: - _eventSink([FlutterError errorWithCode:@"UNAVAILABLE" - message:@"Charging status unavailable" - details:nil]); - break; - } -} - -- (int)getBatteryLevel { - UIDevice* device = UIDevice.currentDevice; - device.batteryMonitoringEnabled = YES; - if (device.batteryState == UIDeviceBatteryStateUnknown) { - return -1; - } else { - return ((int)(device.batteryLevel * 100)); - } -} - -#pragma mark FlutterStreamHandler impl - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _eventSink = eventSink; - [[UIDevice currentDevice] setBatteryMonitoringEnabled:YES]; - [self sendBatteryStateEvent]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onBatteryStateDidChange:) - name:UIDeviceBatteryStateDidChangeNotification - object:nil]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - _eventSink = nil; - return nil; -} - -@end diff --git a/packages/battery/battery/ios/battery.podspec b/packages/battery/battery/ios/battery.podspec deleted file mode 100644 index 83cea45df644..000000000000 --- a/packages/battery/battery/ios/battery.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'battery' - s.version = '0.0.1' - s.summary = 'Flutter plugin for accessing information about the battery.' - s.description = <<-DESC -A Flutter plugin to access various information about the battery of the device the app is running on. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/battery' } - s.documentation_url = 'https://pub.dev/packages/battery' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/battery/battery/lib/battery.dart b/packages/battery/battery/lib/battery.dart deleted file mode 100644 index 92e54be095fe..000000000000 --- a/packages/battery/battery/lib/battery.dart +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:battery_platform_interface/battery_platform_interface.dart'; - -export 'package:battery_platform_interface/battery_platform_interface.dart' - show BatteryState; - -/// API for accessing information about the battery of the device the Flutter -/// app is currently running on. -class Battery { - /// Returns the current battery level in percent. - Future get batteryLevel async => - await BatteryPlatform.instance.batteryLevel(); - - /// Fires whenever the battery state changes. - Stream get onBatteryStateChanged => - BatteryPlatform.instance.onBatteryStateChanged(); -} diff --git a/packages/battery/battery/pubspec.yaml b/packages/battery/battery/pubspec.yaml deleted file mode 100644 index 05226e3f8029..000000000000 --- a/packages/battery/battery/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: battery -description: Flutter plugin for accessing information about the battery state - (full, charging, discharging) on Android and iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/battery/battery -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+battery%22 -version: 2.0.3 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.battery - pluginClass: BatteryPlugin - ios: - pluginClass: FLTBatteryPlugin - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - battery_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - plugin_platform_interface: ^2.0.0 - integration_test: - sdk: flutter - pedantic: ^1.10.0 - test: ^1.16.3 diff --git a/packages/battery/battery/test/battery_test.dart b/packages/battery/battery/test/battery_test.dart deleted file mode 100644 index 8870acb775de..000000000000 --- a/packages/battery/battery/test/battery_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:battery/battery.dart'; -import 'package:battery_platform_interface/battery_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; - -void main() { - group('battery', () { - late Battery battery; - MockBatteryPlatform fakePlatform; - setUp(() async { - fakePlatform = MockBatteryPlatform(); - BatteryPlatform.instance = fakePlatform; - battery = Battery(); - }); - test('batteryLevel', () async { - int result = await battery.batteryLevel; - expect(result, 42); - }); - test('onBatteryStateChanged', () async { - BatteryState result = await battery.onBatteryStateChanged.first; - expect(result, BatteryState.full); - }); - }); -} - -class MockBatteryPlatform extends Fake - with MockPlatformInterfaceMixin - implements BatteryPlatform { - Future batteryLevel() async { - return 42; - } - - Stream onBatteryStateChanged() { - StreamController result = StreamController(); - result.add(BatteryState.full); - return result.stream; - } -} diff --git a/packages/battery/battery_platform_interface/CHANGELOG.md b/packages/battery/battery_platform_interface/CHANGELOG.md deleted file mode 100644 index a9106dd78ce9..000000000000 --- a/packages/battery/battery_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.1 - -- Update Flutter SDK constraint. - -## 1.0.0 - -- Initial open-source release. diff --git a/packages/battery/battery_platform_interface/README.md b/packages/battery/battery_platform_interface/README.md deleted file mode 100644 index e1a42571c6b3..000000000000 --- a/packages/battery/battery_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# battery_platform_interface - -A common platform interface for the [`battery`][1] plugin. - -This interface allows platform-specific implementations of the `battery` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `battery`, extend -[`BatteryPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`BatteryPlatform` by calling -`BatteryPlatform.instance = MyPlatformBattery()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../battery -[2]: lib/battery_platform_interface.dart diff --git a/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart b/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart deleted file mode 100644 index 548edf8257f5..000000000000 --- a/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'method_channel/method_channel_battery.dart'; -import 'enums/battery_state.dart'; - -export 'enums/battery_state.dart'; - -/// The interface that implementations of battery must implement. -/// -/// Platform implementations should extend this class rather than implement it as `battery` -/// 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 -/// [BatteryPlatform] methods. -abstract class BatteryPlatform extends PlatformInterface { - /// Constructs a BatteryPlatform. - BatteryPlatform() : super(token: _token); - - static final Object _token = Object(); - - static BatteryPlatform _instance = MethodChannelBattery(); - - /// The default instance of [BatteryPlatform] to use. - /// - /// Defaults to [MethodChannelBattery]. - static BatteryPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [BatteryPlatform] when they register themselves. - static set instance(BatteryPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Gets the battery level from device. - Future batteryLevel() { - throw UnimplementedError('batteryLevel() has not been implemented.'); - } - - /// gets battery state from device. - Stream onBatteryStateChanged() { - throw UnimplementedError( - 'onBatteryStateChanged() has not been implemented.'); - } -} diff --git a/packages/battery/battery_platform_interface/lib/enums/battery_state.dart b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart deleted file mode 100644 index a525e78ccdf5..000000000000 --- a/packages/battery/battery_platform_interface/lib/enums/battery_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// Indicates the current battery state. -enum BatteryState { - /// The battery is completely full of energy. - full, - - /// The battery is currently storing energy. - charging, - - /// The battery is currently losing energy. - discharging -} diff --git a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart deleted file mode 100644 index 1d0a8329c257..000000000000 --- a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; - -import 'package:battery_platform_interface/battery_platform_interface.dart'; - -import '../battery_platform_interface.dart'; - -/// An implementation of [BatteryPlatform] that uses method channels. -class MethodChannelBattery extends BatteryPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); - - /// The event channel used to interact with the native platform. - @visibleForTesting - final EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); - - /// Method channel for getting battery level. - Future batteryLevel() async { - return (await channel.invokeMethod('getBatteryLevel')).toInt(); - } - - /// Stream variable for storing battery state. - Stream? _onBatteryStateChanged; - - /// Event channel for getting battery change state. - Stream onBatteryStateChanged() { - if (_onBatteryStateChanged == null) { - _onBatteryStateChanged = eventChannel - .receiveBroadcastStream() - .map((dynamic event) => _parseBatteryState(event)); - } - - return _onBatteryStateChanged!; - } -} - -/// Method for parsing battery state. -BatteryState _parseBatteryState(String state) { - switch (state) { - case 'full': - return BatteryState.full; - case 'charging': - return BatteryState.charging; - case 'discharging': - return BatteryState.discharging; - default: - throw ArgumentError('$state is not a valid BatteryState.'); - } -} diff --git a/packages/battery/battery_platform_interface/pubspec.yaml b/packages/battery/battery_platform_interface/pubspec.yaml deleted file mode 100644 index 461cd0bd88ea..000000000000 --- a/packages/battery/battery_platform_interface/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: battery_platform_interface -description: A common platform interface for the battery plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/battery/battery_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+battery%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.1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart deleted file mode 100644 index 697582843a95..000000000000 --- a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart +++ /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 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:battery_platform_interface/battery_platform_interface.dart'; - -import 'package:battery_platform_interface/method_channel/method_channel_battery.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelBattery', () { - late MethodChannelBattery methodChannelBattery; - - setUp(() async { - methodChannelBattery = MethodChannelBattery(); - - methodChannelBattery.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getBatteryLevel': - return 90; - default: - return null; - } - }); - - MethodChannel(methodChannelBattery.eventChannel.name) - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'listen': - await _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - methodChannelBattery.eventChannel.name, - methodChannelBattery.eventChannel.codec - .encodeSuccessEnvelope('full'), - (_) {}, - ); - break; - case 'cancel': - default: - return null; - } - }); - }); - - /// Test for batetry level call. - test('getBatteryLevel', () async { - final int result = await methodChannelBattery.batteryLevel(); - expect(result, 90); - }); - - /// Test for battery changed state call. - test('onBatteryChanged', () async { - final BatteryState result = - await methodChannelBattery.onBatteryStateChanged().first; - expect(result, BatteryState.full); - }); - }); -} - -/// 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/analysis_options.yaml b/packages/camera/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/camera/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 62b5f1f9bd4c..1dc9270824d8 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,148 @@ +## 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. + +## 0.9.4+13 + +* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup. + +## 0.9.4+12 + +* Skips unnecessary AppDelegate setup for unit tests on iOS. +* Internal code cleanup for stricter analysis options. + +## 0.9.4+11 + +* Manages iOS camera's orientation-related states on a background queue to prevent potential race conditions. + +## 0.9.4+10 + +* iOS performance improvement by moving file writing from the main queue to a background IO queue. + +## 0.9.4+9 + +* iOS performance improvement by moving sample buffer handling from the main queue to a background session queue. +* Minor iOS internal code cleanup related to camera class and its delegate. +* Minor iOS internal code cleanup related to resolution preset, video format, focus mode, exposure mode and device orientation. +* Minor iOS internal code cleanup related to flash mode. + +## 0.9.4+8 + +* Fixes a bug where ImageFormatGroup was ignored in `startImageStream` on iOS. + +## 0.9.4+7 + +* Fixes a crash in iOS when passing null queue pointer into AVFoundation API due to race condition. +* Minor iOS internal code cleanup related to dispatch queue. + +## 0.9.4+6 + +* Fixes a crash in iOS when using image stream due to calling Flutter engine API on non-main thread. + +## 0.9.4+5 + +* Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception. +* Fixes integration tests. + +## 0.9.4+4 + +* Change Android compileSdkVersion to 31. +* Remove usages of deprecated Android API `CamcorderProfile`. +* Update gradle version to 7.0.2 on Android. + +## 0.9.4+3 + +* Fix registerTexture and result being called on background thread on iOS. + +## 0.9.4+2 + +* Updated package description; +* Refactor unit test on iOS to make it compatible with new restrictions in Xcode 13 which only supports the use of the `XCUIDevice` in Xcode UI tests. + +## 0.9.4+1 + +* Fixed Android implementation throwing IllegalStateException when switching to a different activity. + ## 0.9.4 * Add web support by endorsing `package:camera_web`. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 24566e76bbfc..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/master/packages/camera/camera/example). +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/build.gradle b/packages/camera/camera/android/build.gradle deleted file mode 100644 index 61d13e5579cc..000000000000 --- a/packages/camera/camera/android/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -group 'io.flutter.plugins.camera' -version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - baseline file("lint-baseline.xml") - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - compileOnly 'androidx.annotation:annotation:1.1.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.11.1' - testImplementation 'androidx.test:core:1.3.0' - testImplementation 'org.robolectric:robolectric:4.3' -} 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/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java deleted file mode 100644 index 7d60e0fffa5c..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.Manifest; -import android.Manifest.permission; -import android.app.Activity; -import android.content.pm.PackageManager; -import androidx.annotation.VisibleForTesting; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -final class CameraPermissions { - interface PermissionsRegistry { - @SuppressWarnings("deprecation") - void addListener( - io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener handler); - } - - interface ResultCallback { - void onResult(String errorCode, String errorDescription); - } - - private static final int CAMERA_REQUEST_ID = 9796; - private boolean ongoing = false; - - void requestPermissions( - Activity activity, - PermissionsRegistry permissionsRegistry, - boolean enableAudio, - ResultCallback callback) { - if (ongoing) { - callback.onResult("cameraPermission", "Camera permission request ongoing"); - } - if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { - permissionsRegistry.addListener( - new CameraRequestPermissionsListener( - (String errorCode, String errorDescription) -> { - ongoing = false; - callback.onResult(errorCode, errorDescription); - })); - ongoing = true; - ActivityCompat.requestPermissions( - activity, - enableAudio - ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO} - : new String[] {Manifest.permission.CAMERA}, - CAMERA_REQUEST_ID); - } else { - // Permissions already exist. Call the callback with success. - callback.onResult(null, null); - } - } - - private boolean hasCameraPermission(Activity activity) { - return ContextCompat.checkSelfPermission(activity, permission.CAMERA) - == PackageManager.PERMISSION_GRANTED; - } - - private boolean hasAudioPermission(Activity activity) { - return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) - == PackageManager.PERMISSION_GRANTED; - } - - @VisibleForTesting - @SuppressWarnings("deprecation") - static final class CameraRequestPermissionsListener - implements io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener { - - // There's no way to unregister permission listeners in the v1 embedding, so we'll be called - // duplicate times in cases where the user denies and then grants a permission. Keep track of if - // we've responded before and bail out of handling the callback manually if this is a repeat - // call. - boolean alreadyCalled = false; - - final ResultCallback callback; - - @VisibleForTesting - CameraRequestPermissionsListener(ResultCallback callback) { - this.callback = callback; - } - - @Override - public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { - if (alreadyCalled || id != CAMERA_REQUEST_ID) { - return false; - } - - alreadyCalled = true; - if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { - callback.onResult("cameraPermission", "MediaRecorderCamera permission not granted"); - } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) { - callback.onResult("cameraPermission", "MediaRecorderAudio permission not granted"); - } else { - callback.onResult(null, null); - } - return true; - } - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java deleted file mode 100644 index 5e25353cbca9..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ /dev/null @@ -1,426 +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 android.app.Activity; -import android.hardware.camera2.CameraAccessException; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; -import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; -import io.flutter.plugins.camera.features.Point; -import io.flutter.plugins.camera.features.autofocus.FocusMode; -import io.flutter.plugins.camera.features.exposurelock.ExposureMode; -import io.flutter.plugins.camera.features.flash.FlashMode; -import io.flutter.plugins.camera.features.resolution.ResolutionPreset; -import io.flutter.view.TextureRegistry; -import java.util.HashMap; -import java.util.Map; - -final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, LifecycleObserver { - private final Activity activity; - private final BinaryMessenger messenger; - private final CameraPermissions cameraPermissions; - private final PermissionsRegistry permissionsRegistry; - private final TextureRegistry textureRegistry; - private final MethodChannel methodChannel; - private final EventChannel imageStreamChannel; - private final Lifecycle lifecycle; - private @Nullable Camera camera; - - MethodCallHandlerImpl( - Activity activity, - BinaryMessenger messenger, - CameraPermissions cameraPermissions, - PermissionsRegistry permissionsAdder, - TextureRegistry textureRegistry, - @Nullable Lifecycle lifecycle) { - this.activity = activity; - this.messenger = messenger; - this.cameraPermissions = cameraPermissions; - this.permissionsRegistry = permissionsAdder; - this.textureRegistry = textureRegistry; - this.lifecycle = lifecycle; - - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); - imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { - switch (call.method) { - case "availableCameras": - try { - result.success(CameraUtils.getAvailableCameras(activity)); - } catch (Exception e) { - handleException(e, result); - } - break; - case "create": - { - if (camera != null) { - camera.close(); - } - - cameraPermissions.requestPermissions( - activity, - permissionsRegistry, - call.argument("enableAudio"), - (String errCode, String errDesc) -> { - if (errCode == null) { - try { - instantiateCamera(call, result); - } catch (Exception e) { - handleException(e, result); - } - } else { - result.error(errCode, errDesc, null); - } - }); - break; - } - case "initialize": - { - if (camera != null) { - try { - camera.open(call.argument("imageFormatGroup")); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - } else { - result.error( - "cameraNotFound", - "Camera not found. Please call the 'create' method before calling 'initialize'.", - null); - } - break; - } - case "takePicture": - { - camera.takePicture(result); - break; - } - case "prepareForVideoRecording": - { - // This optimization is not required for Android. - result.success(null); - break; - } - case "startVideoRecording": - { - camera.startVideoRecording(result); - break; - } - case "stopVideoRecording": - { - camera.stopVideoRecording(result); - break; - } - case "pauseVideoRecording": - { - camera.pauseVideoRecording(result); - break; - } - case "resumeVideoRecording": - { - camera.resumeVideoRecording(result); - break; - } - case "setFlashMode": - { - String modeStr = call.argument("mode"); - FlashMode mode = FlashMode.getValueForString(modeStr); - if (mode == null) { - result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); - return; - } - try { - camera.setFlashMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposureMode": - { - String modeStr = call.argument("mode"); - ExposureMode mode = ExposureMode.getValueForString(modeStr); - if (mode == null) { - result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); - return; - } - try { - camera.setExposureMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposurePoint": - { - Boolean reset = call.argument("reset"); - Double x = null; - Double y = null; - if (reset == null || !reset) { - x = call.argument("x"); - y = call.argument("y"); - } - try { - camera.setExposurePoint(result, new Point(x, y)); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMinExposureOffset": - { - try { - result.success(camera.getMinExposureOffset()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMaxExposureOffset": - { - try { - result.success(camera.getMaxExposureOffset()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getExposureOffsetStepSize": - { - try { - result.success(camera.getExposureOffsetStepSize()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setExposureOffset": - { - try { - camera.setExposureOffset(result, call.argument("offset")); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setFocusMode": - { - String modeStr = call.argument("mode"); - FocusMode mode = FocusMode.getValueForString(modeStr); - if (mode == null) { - result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); - return; - } - try { - camera.setFocusMode(result, mode); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setFocusPoint": - { - Boolean reset = call.argument("reset"); - Double x = null; - Double y = null; - if (reset == null || !reset) { - x = call.argument("x"); - y = call.argument("y"); - } - try { - camera.setFocusPoint(result, new Point(x, y)); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "startImageStream": - { - try { - camera.startPreviewWithImageStream(imageStreamChannel); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "stopImageStream": - { - try { - camera.startPreview(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMaxZoomLevel": - { - assert camera != null; - - try { - float maxZoomLevel = camera.getMaxZoomLevel(); - result.success(maxZoomLevel); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "getMinZoomLevel": - { - assert camera != null; - - try { - float minZoomLevel = camera.getMinZoomLevel(); - result.success(minZoomLevel); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "setZoomLevel": - { - assert camera != null; - - Double zoom = call.argument("zoom"); - - if (zoom == null) { - result.error( - "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); - return; - } - - try { - camera.setZoomLevel(result, zoom.floatValue()); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "lockCaptureOrientation": - { - PlatformChannel.DeviceOrientation orientation = - CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); - - try { - camera.lockCaptureOrientation(orientation); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "unlockCaptureOrientation": - { - try { - camera.unlockCaptureOrientation(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "pausePreview": - { - try { - camera.pausePreview(); - result.success(null); - } catch (Exception e) { - handleException(e, result); - } - break; - } - case "resumePreview": - { - camera.resumePreview(); - result.success(null); - break; - } - case "dispose": - { - if (camera != null) { - camera.dispose(); - } - result.success(null); - break; - } - default: - result.notImplemented(); - break; - } - } - - void stopListening() { - methodChannel.setMethodCallHandler(null); - } - - private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { - String cameraName = call.argument("cameraName"); - String preset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); - - TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = - textureRegistry.createSurfaceTexture(); - DartMessenger dartMessenger = - new DartMessenger( - messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); - CameraProperties cameraProperties = - new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); - ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); - - if (camera != null && lifecycle != null) { - lifecycle.removeObserver(camera); - } - - camera = - new Camera( - activity, - flutterSurfaceTexture, - new CameraFeatureFactoryImpl(), - dartMessenger, - cameraProperties, - resolutionPreset, - enableAudio); - - if (lifecycle != null) { - lifecycle.addObserver(camera); - } - - Map reply = new HashMap<>(); - reply.put("cameraId", flutterSurfaceTexture.id()); - result.success(reply); - } - - // We move catching CameraAccessException out of onMethodCall because it causes a crash - // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to - // to be able to compile with <21 sdks for apps that want the camera and support earlier version. - @SuppressWarnings("ConstantConditions") - private void handleException(Exception exception, Result result) { - if (exception instanceof CameraAccessException) { - result.error("CameraAccess", exception.getMessage(), null); - return; - } - - // CameraAccessException can not be cast to a RuntimeException. - throw (RuntimeException) exception; - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java deleted file mode 100644 index 67763dde9be4..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ /dev/null @@ -1,176 +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.features.resolution; - -import android.hardware.camera2.CaptureRequest; -import android.media.CamcorderProfile; -import android.util.Size; -import androidx.annotation.VisibleForTesting; -import io.flutter.plugins.camera.CameraProperties; -import io.flutter.plugins.camera.features.CameraFeature; - -/** - * Controls the resolutions configuration on the {@link android.hardware.camera2} API. - * - *

The {@link ResolutionFeature} is responsible for converting the platform independent {@link - * ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties - * required to configure the resolution using the {@link android.hardware.camera2} API. - */ -public class ResolutionFeature extends CameraFeature { - private Size captureSize; - private Size previewSize; - private CamcorderProfile recordingProfile; - private ResolutionPreset currentSetting; - private int cameraId; - - /** - * Creates a new instance of the {@link ResolutionFeature}. - * - * @param cameraProperties Collection of characteristics for the current camera device. - * @param resolutionPreset Platform agnostic enum containing resolution information. - * @param cameraName Camera identifier of the camera for which to configure the resolution. - */ - public ResolutionFeature( - CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { - super(cameraProperties); - this.currentSetting = resolutionPreset; - try { - this.cameraId = Integer.parseInt(cameraName, 10); - } catch (NumberFormatException e) { - this.cameraId = -1; - return; - } - configureResolution(resolutionPreset, cameraId); - } - - /** - * Gets the {@link android.media.CamcorderProfile} containing the information to configure the - * resolution using the {@link android.hardware.camera2} API. - * - * @return Resolution information to configure the {@link android.hardware.camera2} API. - */ - public CamcorderProfile getRecordingProfile() { - return this.recordingProfile; - } - - /** - * Gets the optimal preview size based on the configured resolution. - * - * @return The optimal preview size. - */ - public Size getPreviewSize() { - return this.previewSize; - } - - /** - * Gets the optimal capture size based on the configured resolution. - * - * @return The optimal capture size. - */ - public Size getCaptureSize() { - return this.captureSize; - } - - @Override - public String getDebugName() { - return "ResolutionFeature"; - } - - @Override - public ResolutionPreset getValue() { - return currentSetting; - } - - @Override - public void setValue(ResolutionPreset value) { - this.currentSetting = value; - configureResolution(currentSetting, cameraId); - } - - @Override - public boolean checkIsSupported() { - return cameraId >= 0; - } - - @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { - // No-op: when setting a resolution there is no need to update the request builder. - } - - @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - /** - * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link - * ResolutionPreset}. - * - * @param cameraId Camera identifier which indicates the device's camera for which to select a - * {@link android.media.CamcorderProfile}. - * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link - * android.media.CamcorderProfile}. - * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied - * {@link ResolutionPreset}. - */ - public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - int cameraId, ResolutionPreset preset) { - if (cameraId < 0) { - throw new AssertionError( - "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); - } - - switch (preset) { - // All of these cases deliberately fall through to get the best available profile. - case max: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - } - case ultraHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - case veryHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); - } - default: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } else { - throw new IllegalArgumentException( - "No capture session available for current capture session."); - } - } - } - - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { - if (!checkIsSupported()) { - return; - } - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraId, resolutionPreset); - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java deleted file mode 100644 index a78c2b47b7ad..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ /dev/null @@ -1,74 +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.media; - -import android.media.CamcorderProfile; -import android.media.MediaRecorder; -import androidx.annotation.NonNull; -import java.io.IOException; - -public class MediaRecorderBuilder { - static class MediaRecorderFactory { - MediaRecorder makeMediaRecorder() { - return new MediaRecorder(); - } - } - - private final String outputFilePath; - private final CamcorderProfile recordingProfile; - private final MediaRecorderFactory recorderFactory; - - private boolean enableAudio; - private int mediaOrientation; - - public MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) { - this(recordingProfile, outputFilePath, new MediaRecorderFactory()); - } - - MediaRecorderBuilder( - @NonNull CamcorderProfile recordingProfile, - @NonNull String outputFilePath, - MediaRecorderFactory helper) { - this.outputFilePath = outputFilePath; - this.recordingProfile = recordingProfile; - this.recorderFactory = helper; - } - - public MediaRecorderBuilder setEnableAudio(boolean enableAudio) { - this.enableAudio = enableAudio; - return this; - } - - public MediaRecorderBuilder setMediaOrientation(int orientation) { - this.mediaOrientation = orientation; - return this; - } - - public MediaRecorder build() throws IOException { - MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); - - // There's a fixed order that mediaRecorder expects. Only change these functions accordingly. - // You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder. - if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(recordingProfile.fileFormat); - if (enableAudio) { - mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); - mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate); - mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); - } - mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); - mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); - mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); - mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - mediaRecorder.setOutputFile(outputFilePath); - mediaRecorder.setOrientationHint(this.mediaOrientation); - - mediaRecorder.prepare(); - - return mediaRecorder; - } -} 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/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java deleted file mode 100644 index 6b714ce41e34..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ /dev/null @@ -1,55 +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 org.junit.Assert.assertEquals; - -import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import org.junit.Test; - -public class CameraUtilsTest { - - @Test - public void serializeDeviceOrientation_serializesCorrectly() { - assertEquals( - "portraitUp", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); - assertEquals( - "portraitDown", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); - assertEquals( - "landscapeLeft", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); - assertEquals( - "landscapeRight", - CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); - } - - @Test(expected = UnsupportedOperationException.class) - public void serializeDeviceOrientation_throws_for_null() { - CameraUtils.serializeDeviceOrientation(null); - } - - @Test - public void deserializeDeviceOrientation_deserializesCorrectly() { - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.deserializeDeviceOrientation("portraitUp")); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.deserializeDeviceOrientation("portraitDown")); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.deserializeDeviceOrientation("landscapeLeft")); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.deserializeDeviceOrientation("landscapeRight")); - } - - @Test(expected = UnsupportedOperationException.class) - public void deserializeDeviceOrientation_throwsForNull() { - CameraUtils.deserializeDeviceOrientation(null); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java deleted file mode 100644 index 35eed7a66a1a..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java +++ /dev/null @@ -1,69 +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 org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.app.Activity; -import android.hardware.camera2.CameraAccessException; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.camera.utils.TestUtils; -import io.flutter.view.TextureRegistry; -import org.junit.Before; -import org.junit.Test; - -public class MethodCallHandlerImplTest { - - MethodChannel.MethodCallHandler handler; - MethodChannel.Result mockResult; - Camera mockCamera; - - @Before - public void setUp() { - handler = - new MethodCallHandlerImpl( - mock(Activity.class), - mock(BinaryMessenger.class), - mock(CameraPermissions.class), - mock(CameraPermissions.PermissionsRegistry.class), - mock(TextureRegistry.class), - null); - mockResult = mock(MethodChannel.Result.class); - mockCamera = mock(Camera.class); - TestUtils.setPrivateField(handler, "camera", mockCamera); - } - - @Test - public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() - throws CameraAccessException { - handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); - - verify(mockCamera, times(1)).pausePreview(); - verify(mockResult, times(1)).success(null); - } - - @Test - public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException() - throws CameraAccessException { - doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview(); - - handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); - - verify(mockResult, times(1)).error("CameraAccess", null, null); - } - - @Test - public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() { - handler.onMethodCall(new MethodCall("resumePreview", null), mockResult); - - verify(mockCamera, times(1)).resumePreview(); - verify(mockResult, times(1)).success(null); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java deleted file mode 100644 index e09223dfabe9..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ /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. - -package io.flutter.plugins.camera.features.resolution; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; - -import android.media.CamcorderProfile; -import io.flutter.plugins.camera.CameraProperties; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; - -public class ResolutionFeatureTest { - private static final String cameraName = "1"; - private CamcorderProfile mockProfileLow; - private MockedStatic mockedStaticProfile; - - @Before - public void before() { - mockedStaticProfile = mockStatic(CamcorderProfile.class); - mockProfileLow = mock(CamcorderProfile.class); - CamcorderProfile mockProfile = mock(CamcorderProfile.class); - - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) - .thenReturn(true); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) - .thenReturn(true); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) - .thenReturn(true); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) - .thenReturn(true); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) - .thenReturn(true); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) - .thenReturn(true); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) - .thenReturn(true); - - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) - .thenReturn(mockProfile); - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) - .thenReturn(mockProfile); - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) - .thenReturn(mockProfile); - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) - .thenReturn(mockProfile); - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) - .thenReturn(mockProfile); - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) - .thenReturn(mockProfile); - mockedStaticProfile - .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) - .thenReturn(mockProfileLow); - } - - @After - public void after() { - mockedStaticProfile.reset(); - mockedStaticProfile.close(); - } - - @Test - public void getDebugName_shouldReturnTheNameOfTheFeature() { - CameraProperties mockCameraProperties = mock(CameraProperties.class); - ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); - - assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); - } - - @Test - public void getValue_shouldReturnInitialValueWhenNotSet() { - CameraProperties mockCameraProperties = mock(CameraProperties.class); - ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); - - assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); - } - - @Test - public void getValue_shouldEchoSetValue() { - CameraProperties mockCameraProperties = mock(CameraProperties.class); - ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); - - resolutionFeature.setValue(ResolutionPreset.high); - - assertEquals(ResolutionPreset.high, resolutionFeature.getValue()); - } - - @Test - public void checkIsSupport_returnsTrue() { - CameraProperties mockCameraProperties = mock(CameraProperties.class); - ResolutionFeature resolutionFeature = - new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); - - assertTrue(resolutionFeature.checkIsSupported()); - } - - @Test - public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) - .thenReturn(false); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) - .thenReturn(false); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) - .thenReturn(false); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) - .thenReturn(false); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) - .thenReturn(false); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) - .thenReturn(false); - mockedStaticProfile - .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) - .thenReturn(true); - - assertEquals( - mockProfileLow, - ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( - 1, ResolutionPreset.max)); - } - - @Test - public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); - - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); - } - - @Test - public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); - - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); - } - - @Test - public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); - - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); - } - - @Test - public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); - - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); - } - - @Test - public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); - - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); - } - - @Test - public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); - - mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java deleted file mode 100644 index 5425409c2f3a..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ /dev/null @@ -1,106 +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.media; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -import android.media.CamcorderProfile; -import android.media.MediaRecorder; -import java.io.IOException; -import java.lang.reflect.Constructor; -import org.junit.Test; -import org.mockito.InOrder; - -public class MediaRecorderBuilderTest { - @Test - public void ctor_test() { - MediaRecorderBuilder builder = - new MediaRecorderBuilder(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P), ""); - - assertNotNull(builder); - } - - @Test - public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { - CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); - MediaRecorderBuilder.MediaRecorderFactory mockFactory = - mock(MediaRecorderBuilder.MediaRecorderFactory.class); - MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); - String outputFilePath = "mock_video_file_path"; - int mediaOrientation = 1; - MediaRecorderBuilder builder = - new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) - .setEnableAudio(false) - .setMediaOrientation(mediaOrientation); - - when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); - - MediaRecorder recorder = builder.build(); - - InOrder inOrder = inOrder(recorder); - inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); - inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); - inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); - inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); - inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); - inOrder - .verify(recorder) - .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); - inOrder.verify(recorder).setOutputFile(outputFilePath); - inOrder.verify(recorder).setOrientationHint(mediaOrientation); - inOrder.verify(recorder).prepare(); - } - - @Test - public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { - CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); - MediaRecorderBuilder.MediaRecorderFactory mockFactory = - mock(MediaRecorderBuilder.MediaRecorderFactory.class); - MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); - String outputFilePath = "mock_video_file_path"; - int mediaOrientation = 1; - MediaRecorderBuilder builder = - new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) - .setEnableAudio(true) - .setMediaOrientation(mediaOrientation); - - when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); - - MediaRecorder recorder = builder.build(); - - InOrder inOrder = inOrder(recorder); - inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); - inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); - inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); - inOrder.verify(recorder).setAudioEncoder(recorderProfile.audioCodec); - inOrder.verify(recorder).setAudioEncodingBitRate(recorderProfile.audioBitRate); - inOrder.verify(recorder).setAudioSamplingRate(recorderProfile.audioSampleRate); - inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); - inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); - inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); - inOrder - .verify(recorder) - .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); - inOrder.verify(recorder).setOutputFile(outputFilePath); - inOrder.verify(recorder).setOrientationHint(mediaOrientation); - inOrder.verify(recorder).prepare(); - } - - private CamcorderProfile getEmptyCamcorderProfile() { - try { - Constructor constructor = - CamcorderProfile.class.getDeclaredConstructor( - int.class, int.class, int.class, int.class, int.class, int.class, int.class, - int.class, int.class, int.class, int.class, int.class); - - constructor.setAccessible(true); - return constructor.newInstance(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - } catch (Exception ignored) { - } - - return null; - } -} diff --git a/packages/camera/camera/camera_android.iml b/packages/camera/camera/camera_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/camera/camera_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/camera/camera/example/android.iml b/packages/camera/camera/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/camera/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 7d0e281b74e8..5d6af5887012 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -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/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java b/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java rename to packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java similarity index 100% rename from packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java rename to packages/camera/camera/example/android/app/src/androidTest/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index a6738207fd15..b253d8e5f746 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=false android.enableR8=true 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/camera_example.iml b/packages/camera/camera/example/camera_example.iml deleted file mode 100644 index dafb001137cd..000000000000 --- a/packages/camera/camera/example/camera_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/camera/camera/example/camera_example_android.iml b/packages/camera/camera/example/camera_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/camera/example/camera_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index 00a1714f9a5a..557f4858acab 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -9,9 +9,9 @@ import 'dart:ui'; import 'package:camera/camera.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'; -import 'package:integration_test/integration_test.dart'; void main() { late Directory testDir; @@ -59,7 +59,7 @@ void main() { 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Picture - final file = await controller.takePicture(); + final XFile file = await controller.takePicture(); // Load picture final File fileImage = File(file.path); @@ -71,28 +71,32 @@ void main() { expectedSize, Size(image.height.toDouble(), image.width.toDouble())); } - testWidgets('Capture specific image resolutions', - (WidgetTester tester) async { - final List cameras = await availableCameras(); - if (cameras.isEmpty) { - return; - } - for (CameraDescription cameraDescription in cameras) { - bool previousPresetExactlySupported = true; - for (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(); + testWidgets( + 'Capture specific image resolutions', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; } - } - }, skip: !Platform.isAndroid); + 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 @@ -106,7 +110,7 @@ void main() { // Take Video await controller.startVideoRecording(); sleep(const Duration(milliseconds: 300)); - final file = await controller.stopVideoRecording(); + final XFile file = await controller.stopVideoRecording(); // Load video metadata final File videoFile = File(file.path); @@ -121,29 +125,33 @@ void main() { expectedSize, Size(video.height, video.width)); } - testWidgets('Capture specific video resolutions', - (WidgetTester tester) async { - final List cameras = await availableCameras(); - if (cameras.isEmpty) { - return; - } - for (CameraDescription cameraDescription in cameras) { - bool previousPresetExactlySupported = true; - for (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( + 'Capture specific video resolutions', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; } - } - }, skip: !Platform.isAndroid); + 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 availableCameras(); @@ -183,7 +191,7 @@ void main() { sleep(const Duration(milliseconds: 500)); - final file = await controller.stopVideoRecording(); + final XFile file = await controller.stopVideoRecording(); final int recordingTime = DateTime.now().millisecondsSinceEpoch - recordingStart; @@ -216,7 +224,9 @@ void main() { bool _isDetecting = false; await controller.startImageStream((CameraImage image) { - if (_isDetecting) return; + if (_isDetecting) { + return; + } _isDetecting = true; @@ -232,4 +242,56 @@ void main() { }, skip: !Platform.isAndroid, ); + + /// 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((CameraImage image) { + if (!_completer.isCompleted) { + Future(() async { + await controller.stopImageStream(); + await controller.dispose(); + }).then((Object? value) { + _completer.complete(image); + }); + } + }); + return _completer.future; + } + + testWidgets( + 'iOS image streaming with imageFormatGroup', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + 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.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 8520bb00fb2f..99433b084f27 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,30 +7,16 @@ objects = { /* Begin PBXBuildFile section */ - 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB766A2665316900CE5A93 /* CameraFocusTests.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D82918721FABF772705DB0 /* libPods-Runner.a */; }; 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 */; }; - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; }; - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; }; - E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.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; @@ -45,20 +31,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 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 = ""; }; 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 = ""; }; - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; - 40D9DDFB3787960D28DF3FB3 /* 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 = ""; }; - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; 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 = ""; }; - 8F7D83D0CFC9B51065F87CE1 /* 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 = ""; }; + 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; }; @@ -67,47 +49,27 @@ 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 = ""; }; - A4725B4F24805CD3CA67828F /* 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 = ""; }; - D1FF8C34CA9E9BE702C5EC06 /* 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 = ""; }; - E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 03BB76652665316900CE5A93 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */, + 236906D1621AE863A5B2E770 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 03BB76692665316900CE5A93 /* RunnerTests */ = { + 3242FD2B467C15C62200632F /* Frameworks */ = { isa = PBXGroup; children = ( - 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, - 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, - 03BB766C2665316900CE5A93 /* Info.plist */, - E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 78D1009194BD06C03BED950D /* Frameworks */ = { - isa = PBXGroup; - children = ( - 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */, - 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */, + 89D82918721FABF772705DB0 /* libPods-Runner.a */, + 1944D8072499F3B5E7653D44 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -128,10 +90,9 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 03BB76692665316900CE5A93 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, FD386F00E98D73419C929072 /* Pods */, - 78D1009194BD06C03BED950D /* Frameworks */, + 3242FD2B467C15C62200632F /* Frameworks */, ); sourceTree = ""; }; @@ -139,7 +100,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 03BB76682665316900CE5A93 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -171,10 +131,10 @@ FD386F00E98D73419C929072 /* Pods */ = { isa = PBXGroup; children = ( - 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */, - A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */, - 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */, - D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */, + 59848A7CA98C1FADF8840207 /* Pods-Runner.debug.xcconfig */, + 14AE82C910C2A12F2ECB2094 /* Pods-Runner.release.xcconfig */, + 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */, + A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -182,30 +142,11 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 03BB76672665316900CE5A93 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 604FC00FF5713F40F2A4441D /* [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 = ( - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */, + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -228,14 +169,9 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { - 03BB76672665316900CE5A93 = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -255,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; @@ -282,43 +210,35 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1D0D227A6719C1144CAE5AB5 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Thin Binary"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 604FC00FF5713F40F2A4441D /* [CP] Check Pods Manifest.lock */ = { + 9872F2A25E8A171A111468CD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -333,40 +253,16 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(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"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 03BB76642665316900CE5A93 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, - E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, - 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -379,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; @@ -407,55 +295,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 03BB766F2665316900CE5A93 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 40D9DDFB3787960D28DF3FB3 /* 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; - 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 = D1FF8C34CA9E9BE702C5EC06 /* 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; - 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 = { @@ -567,6 +406,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -588,6 +428,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -607,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.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1447e08231be..f4b3c1099001 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + // 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/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m deleted file mode 100644 index 6c29ef7b2866..000000000000 --- a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import camera; -@import XCTest; - -#import - -@interface CameraOrientationTests : XCTestCase -@property(strong, nonatomic) id mockRegistrar; -@property(strong, nonatomic) id mockMessenger; -@end - -@implementation CameraOrientationTests - -- (void)setUp { - [super setUp]; - self.mockRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); - self.mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - OCMStub([self.mockRegistrar messenger]).andReturn(self.mockMessenger); -} - -- (void)testOrientationNotifications { - id mockMessenger = self.mockMessenger; - [mockMessenger setExpectationOrderMatters:YES]; - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationPortrait; - - [CameraPlugin registerWithRegistrar:self.mockRegistrar]; - - [self rotate:UIDeviceOrientationPortraitUpsideDown expectedChannelOrientation:@"portraitDown"]; - [self rotate:UIDeviceOrientationPortrait expectedChannelOrientation:@"portraitUp"]; - [self rotate:UIDeviceOrientationLandscapeRight expectedChannelOrientation:@"landscapeLeft"]; - [self rotate:UIDeviceOrientationLandscapeLeft expectedChannelOrientation:@"landscapeRight"]; - - OCMReject([mockMessenger sendOnChannel:[OCMArg any] message:[OCMArg any]]); - // No notification when orientation doesn't change. - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationLandscapeLeft; - // No notification when flat. - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationFaceUp; - // No notification when facedown. - XCUIDevice.sharedDevice.orientation = UIDeviceOrientationFaceDown; - - OCMVerifyAll(mockMessenger); -} - -- (void)rotate:(UIDeviceOrientation)deviceOrientation - expectedChannelOrientation:(NSString*)channelOrientation { - id mockMessenger = self.mockMessenger; - XCTestExpectation* orientationExpectation = [self expectationWithDescription:channelOrientation]; - - OCMExpect([mockMessenger - sendOnChannel:[OCMArg any] - message:[OCMArg checkWithBlock:^BOOL(NSData* data) { - NSObject* codec = [FlutterStandardMethodCodec sharedInstance]; - FlutterMethodCall* methodCall = [codec decodeMethodCall:data]; - [orientationExpectation fulfill]; - return - [methodCall.method isEqualToString:@"orientation_changed"] && - [methodCall.arguments isEqualToDictionary:@{@"orientation" : channelOrientation}]; - }]]); - - XCUIDevice.sharedDevice.orientation = deviceOrientation; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; -} - -@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m deleted file mode 100644 index 549b40a52e46..000000000000 --- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import camera; -@import XCTest; -@import AVFoundation; -#import - -@interface FLTCam : NSObject -@property(assign, nonatomic) BOOL isPreviewPaused; -- (void)pausePreviewWithResult:(FlutterResult)result; -- (void)resumePreviewWithResult:(FlutterResult)result; -@end - -@interface CameraPreviewPauseTests : XCTestCase -@property(readonly, nonatomic) FLTCam* camera; -@end - -@implementation CameraPreviewPauseTests - -- (void)setUp { - _camera = [[FLTCam alloc] init]; -} - -- (void)testPausePreviewWithResult_shouldPausePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera pausePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; - XCTAssertTrue(_camera.isPreviewPaused); -} - -- (void)testResumePreviewWithResult_shouldResumePreview { - XCTestExpectation* resultExpectation = - [self expectationWithDescription:@"Succeeding result with nil value"]; - [_camera resumePreviewWithResult:^void(id _Nullable result) { - XCTAssertNil(result); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:2.0 handler:nil]; - XCTAssertFalse(_camera.isPreviewPaused); -} - -@end diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index a3a5d1d46391..b90b0033563a 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 { @@ -105,6 +108,7 @@ class _CameraExampleHomeState extends State 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(), @@ -194,7 +196,8 @@ class _CameraExampleHomeState extends State behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, - onTapDown: (details) => onViewFinderTap(details, constraints), + onTapDown: (TapDownDetails details) => + onViewFinderTap(details, constraints), ); }), ), @@ -228,34 +231,34 @@ class _CameraExampleHomeState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - localVideoController == null && imageFile == null - ? Container() - : SizedBox( - 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( - child: Center( - child: AspectRatio( - aspectRatio: - localVideoController.value.size != null - ? localVideoController - .value.aspectRatio - : 1.0, - child: VideoPlayer(localVideoController)), - ), - decoration: BoxDecoration( - border: Border.all(color: Colors.pink)), - ), - width: 64.0, - height: 64.0, - ), + 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)), + ), + ), + ), ], ), ), @@ -265,34 +268,34 @@ class _CameraExampleHomeState extends State /// Display a bar with buttons to change the flash and exposure modes Widget _modeControlRowWidget() { return Column( - children: [ + children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, children: [ IconButton( - icon: Icon(Icons.flash_on), + 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 - ? [ + ...!kIsWeb + ? [ IconButton( - icon: Icon(Icons.exposure), + icon: const Icon(Icons.exposure), color: Colors.blue, onPressed: controller != null ? onExposureModeButtonPressed : null, ), IconButton( - icon: Icon(Icons.filter_center_focus), + 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, @@ -323,9 +326,9 @@ class _CameraExampleHomeState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, - children: [ + children: [ IconButton( - icon: Icon(Icons.flash_off), + icon: const Icon(Icons.flash_off), color: controller?.value.flashMode == FlashMode.off ? Colors.orange : Colors.blue, @@ -334,7 +337,7 @@ class _CameraExampleHomeState extends State : null, ), IconButton( - icon: Icon(Icons.flash_auto), + icon: const Icon(Icons.flash_auto), color: controller?.value.flashMode == FlashMode.auto ? Colors.orange : Colors.blue, @@ -343,7 +346,7 @@ class _CameraExampleHomeState extends State : null, ), IconButton( - icon: Icon(Icons.flash_on), + icon: const Icon(Icons.flash_on), color: controller?.value.flashMode == FlashMode.always ? Colors.orange : Colors.blue, @@ -352,7 +355,7 @@ class _CameraExampleHomeState extends State : null, ), IconButton( - icon: Icon(Icons.highlight), + icon: const Icon(Icons.highlight), color: controller?.value.flashMode == FlashMode.torch ? Colors.orange : Colors.blue, @@ -368,11 +371,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, @@ -384,16 +391,15 @@ class _CameraExampleHomeState extends State child: Container( color: Colors.grey.shade50, child: Column( - children: [ - Center( - child: Text("Exposure Mode"), + children: [ + const Center( + child: Text('Exposure Mode'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, - children: [ + children: [ TextButton( - child: Text('AUTO'), style: styleAuto, onPressed: controller != null ? () => @@ -405,31 +411,32 @@ class _CameraExampleHomeState extends State showInSnackBar('Resetting exposure point'); } }, + child: const Text('AUTO'), ), TextButton( - child: Text('LOCKED'), style: styleLocked, onPressed: controller != null ? () => onSetExposureModeButtonPressed(ExposureMode.locked) : null, + child: const Text('LOCKED'), ), TextButton( - child: Text('RESET OFFSET'), style: styleLocked, onPressed: controller != null ? () => controller!.setExposureOffset(0.0) : null, + child: const Text('RESET OFFSET'), ), ], ), - Center( - child: Text("Exposure Offset"), + const Center( + child: Text('Exposure Offset'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, - children: [ + children: [ Text(_minAvailableExposureOffset.toString()), Slider( value: _currentExposureOffset, @@ -453,11 +460,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, @@ -469,31 +480,33 @@ class _CameraExampleHomeState extends State child: Container( color: Colors.grey.shade50, child: Column( - children: [ - Center( - child: Text("Focus Mode"), + children: [ + const Center( + child: Text('Focus Mode'), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, - children: [ + children: [ TextButton( - child: Text('AUTO'), style: styleAuto, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.auto) : null, onLongPress: () { - if (controller != null) controller!.setFocusPoint(null); + if (controller != null) { + controller!.setFocusPoint(null); + } showInSnackBar('Resetting focus point'); }, + child: const Text('AUTO'), ), TextButton( - child: Text('LOCKED'), style: styleLocked, onPressed: controller != null ? () => onSetFocusModeButtonPressed(FocusMode.locked) : null, + child: const Text('LOCKED'), ), ], ), @@ -533,8 +546,8 @@ class _CameraExampleHomeState extends State IconButton( icon: cameraController != null && cameraController.value.isRecordingPaused - ? Icon(Icons.play_arrow) - : Icon(Icons.pause), + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), color: Colors.blue, onPressed: cameraController != null && cameraController.value.isInitialized && @@ -570,18 +583,21 @@ class _CameraExampleHomeState extends State Widget _cameraTogglesRowWidget() { final List toggles = []; - final 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) { + _ambiguate(SchedulerBinding.instance)?.addPostFrameCallback((_) async { + showInSnackBar('No camera found.'); + }); + return const Text('None'); } else { - for (CameraDescription cameraDescription in cameras) { + for (final CameraDescription cameraDescription in _cameras) { toggles.add( SizedBox( width: 90.0, @@ -605,8 +621,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) { @@ -616,7 +632,7 @@ class _CameraExampleHomeState extends State final CameraController cameraController = controller!; - final offset = Offset( + final Offset offset = Offset( details.localPosition.dx / constraints.maxWidth, details.localPosition.dy / constraints.maxHeight, ); @@ -624,9 +640,16 @@ class _CameraExampleHomeState extends State cameraController.setFocusPoint(offset); } - void onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - await controller!.dispose(); + 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( @@ -640,7 +663,9 @@ class _CameraExampleHomeState extends State // If the controller is updated then update the UI. cameraController.addListener(() { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } if (cameraController.value.hasError) { showInSnackBar( 'Camera error ${cameraController.value.errorDescription}'); @@ -649,27 +674,52 @@ class _CameraExampleHomeState extends State try { await cameraController.initialize(); - await Future.wait([ + await Future.wait(>[ // The exposure mode is currently not supported on the web. - ...(!kIsWeb - ? [ - cameraController - .getMinExposureOffset() - .then((value) => _minAvailableExposureOffset = value), + ...!kIsWeb + ? >[ + cameraController.getMinExposureOffset().then( + (double value) => _minAvailableExposureOffset = value), cameraController .getMaxExposureOffset() - .then((value) => _maxAvailableExposureOffset = value) + .then((double value) => _maxAvailableExposureOffset = value) ] - : []), + : >[], cameraController .getMaxZoomLevel() - .then((value) => _maxAvailableZoom = value), + .then((double value) => _maxAvailableZoom = value), cameraController .getMinZoomLevel() - .then((value) => _minAvailableZoom = value), + .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) { @@ -685,7 +735,9 @@ class _CameraExampleHomeState extends State videoController?.dispose(); videoController = null; }); - if (file != null) showInSnackBar('Picture saved to ${file.path}'); + if (file != null) { + showInSnackBar('Picture saved to ${file.path}'); + } } }); } @@ -727,7 +779,7 @@ class _CameraExampleHomeState extends State } } - void onCaptureOrientationLockButtonPressed() async { + Future onCaptureOrientationLockButtonPressed() async { try { if (controller != null) { final CameraController cameraController = controller!; @@ -747,34 +799,44 @@ class _CameraExampleHomeState extends State void onSetFlashModeButtonPressed(FlashMode mode) { setFlashMode(mode).then((_) { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); }); } void onSetExposureModeButtonPressed(ExposureMode mode) { setExposureMode(mode).then((_) { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); }); } void onSetFocusModeButtonPressed(FocusMode mode) { setFocusMode(mode).then((_) { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); }); } void onVideoRecordButtonPressed() { startVideoRecording().then((_) { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } }); } void onStopButtonPressed() { - stopVideoRecording().then((file) { - if (mounted) setState(() {}); + stopVideoRecording().then((XFile? file) { + if (mounted) { + setState(() {}); + } if (file != null) { showInSnackBar('Video recorded to ${file.path}'); videoFile = file; @@ -797,19 +859,25 @@ class _CameraExampleHomeState extends State await cameraController.pausePreview(); } - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } } void onPauseButtonPressed() { pauseVideoRecording().then((_) { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } showInSnackBar('Video recording paused'); }); } void onResumeButtonPressed() { resumeVideoRecording().then((_) { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } showInSnackBar('Video recording resumed'); }); } @@ -854,7 +922,7 @@ class _CameraExampleHomeState extends State final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { - return null; + return; } try { @@ -869,7 +937,7 @@ class _CameraExampleHomeState extends State final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isRecordingVideo) { - return null; + return; } try { @@ -947,7 +1015,9 @@ class _CameraExampleHomeState extends State videoPlayerListener = () { if (videoController != null && videoController!.value.size != null) { // Refreshing the state to update video player with the correct ratio. - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + } videoController!.removeListener(videoPlayerListener!); } }; @@ -977,7 +1047,7 @@ class _CameraExampleHomeState extends State } try { - XFile file = await cameraController.takePicture(); + final XFile file = await cameraController.takePicture(); return file; } on CameraException catch (e) { _showCameraException(e); @@ -986,31 +1056,35 @@ 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?. 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 1899835aca50..e9ae2c74a6be 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: ">=2.8.0" dependencies: camera: @@ -14,19 +14,19 @@ 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: ../ - path_provider: ^2.0.0 flutter: sdk: flutter + path_provider: ^2.0.0 video_player: ^2.1.4 dev_dependencies: - flutter_test: - sdk: flutter + 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/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/example/test_driver/integration_test.dart b/packages/camera/camera/example/test_driver/integration_test.dart index dedb2537fb88..4ec97e66d36c 100644 --- a/packages/camera/camera/example/test_driver/integration_test.dart +++ b/packages/camera/camera/example/test_driver/integration_test.dart @@ -18,7 +18,7 @@ Future main() async { final bool adbExists = Process.runSync('which', ['adb']).exitCode == 0; if (!adbExists) { - print('This test needs ADB to exist on the \$PATH. Skipping...'); + print(r'This test needs ADB to exist on the $PATH. Skipping...'); exit(0); } print('Granting camera permissions...'); @@ -59,6 +59,6 @@ Future main() async { 'android.permission.RECORD_AUDIO' ]); - final Map result = jsonDecode(data); + final Map result = jsonDecode(data) as Map; exit(result['result'] == 'true' ? 0 : 1); } diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m deleted file mode 100644 index da560d6c4df7..000000000000 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ /dev/null @@ -1,1543 +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 "CameraPlugin.h" -#import -#import -#import -#import -#import - -static FlutterError *getFlutterError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; -} - -@interface FLTSavePhotoDelegate : NSObject -@property(readonly, nonatomic) NSString *path; -@property(readonly, nonatomic) FlutterResult result; -@end - -@interface FLTImageStreamHandler : NSObject -@property FlutterEventSink eventSink; -@end - -@implementation FLTImageStreamHandler - -- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { - _eventSink = nil; - return nil; -} - -- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments - eventSink:(nonnull FlutterEventSink)events { - _eventSink = events; - return nil; -} -@end - -@implementation FLTSavePhotoDelegate { - /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. - FLTSavePhotoDelegate *selfReference; -} - -- initWithPath:(NSString *)path result:(FlutterResult)result { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _path = path; - selfReference = self; - _result = result; - return self; -} - -- (void)captureOutput:(AVCapturePhotoOutput *)output - didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer - previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer - resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings - bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings - error:(NSError *)error API_AVAILABLE(ios(10)) { - selfReference = nil; - if (error) { - _result(getFlutterError(error)); - return; - } - - NSData *data = [AVCapturePhotoOutput - JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer - previewPhotoSampleBuffer:previewPhotoSampleBuffer]; - - // TODO(sigurdm): Consider writing file asynchronously. - bool success = [data writeToFile:_path atomically:YES]; - - if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); - return; - } - _result(_path); -} - -- (void)captureOutput:(AVCapturePhotoOutput *)output - didFinishProcessingPhoto:(AVCapturePhoto *)photo - error:(NSError *)error API_AVAILABLE(ios(11.0)) { - selfReference = nil; - if (error) { - _result(getFlutterError(error)); - return; - } - - NSData *photoData = [photo fileDataRepresentation]; - - bool success = [photoData writeToFile:_path atomically:YES]; - if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); - return; - } - _result(_path); -} -@end - -// Mirrors FlashMode in flash_mode.dart -typedef enum { - FlashModeOff, - FlashModeAuto, - FlashModeAlways, - FlashModeTorch, -} FlashMode; - -static FlashMode getFlashModeForString(NSString *mode) { - if ([mode isEqualToString:@"off"]) { - return FlashModeOff; - } else if ([mode isEqualToString:@"auto"]) { - return FlashModeAuto; - } else if ([mode isEqualToString:@"always"]) { - return FlashModeAlways; - } else if ([mode isEqualToString:@"torch"]) { - return FlashModeTorch; - } else { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat:@"Unknown flash mode %@", mode] - }]; - @throw error; - } -} - -static OSType getVideoFormatFromString(NSString *videoFormatString) { - if ([videoFormatString isEqualToString:@"bgra8888"]) { - return kCVPixelFormatType_32BGRA; - } else if ([videoFormatString isEqualToString:@"yuv420"]) { - return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; - } else { - NSLog(@"The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888"); - return kCVPixelFormatType_32BGRA; - } -} - -static AVCaptureFlashMode getAVCaptureFlashModeForFlashMode(FlashMode mode) { - switch (mode) { - case FlashModeOff: - return AVCaptureFlashModeOff; - case FlashModeAuto: - return AVCaptureFlashModeAuto; - case FlashModeAlways: - return AVCaptureFlashModeOn; - case FlashModeTorch: - default: - return -1; - } -} - -// Mirrors ExposureMode in camera.dart -typedef enum { - ExposureModeAuto, - ExposureModeLocked, - -} ExposureMode; - -static NSString *getStringForExposureMode(ExposureMode mode) { - switch (mode) { - case ExposureModeAuto: - return @"auto"; - case ExposureModeLocked: - return @"locked"; - } - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat:@"Unknown string for exposure mode"] - }]; - @throw error; -} - -static ExposureMode getExposureModeForString(NSString *mode) { - if ([mode isEqualToString:@"auto"]) { - return ExposureModeAuto; - } else if ([mode isEqualToString:@"locked"]) { - return ExposureModeLocked; - } else { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat:@"Unknown exposure mode %@", mode] - }]; - @throw error; - } -} - -static UIDeviceOrientation getUIDeviceOrientationForString(NSString *orientation) { - if ([orientation isEqualToString:@"portraitDown"]) { - return UIDeviceOrientationPortraitUpsideDown; - } else if ([orientation isEqualToString:@"landscapeLeft"]) { - return UIDeviceOrientationLandscapeRight; - } else if ([orientation isEqualToString:@"landscapeRight"]) { - return UIDeviceOrientationLandscapeLeft; - } else if ([orientation isEqualToString:@"portraitUp"]) { - return UIDeviceOrientationPortrait; - } else { - NSError *error = [NSError - errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : - [NSString stringWithFormat:@"Unknown device orientation %@", orientation] - }]; - @throw error; - } -} - -static NSString *getStringForUIDeviceOrientation(UIDeviceOrientation orientation) { - switch (orientation) { - case UIDeviceOrientationPortraitUpsideDown: - return @"portraitDown"; - case UIDeviceOrientationLandscapeRight: - return @"landscapeLeft"; - case UIDeviceOrientationLandscapeLeft: - return @"landscapeRight"; - case UIDeviceOrientationPortrait: - default: - return @"portraitUp"; - break; - }; -} - -// Mirrors FocusMode in camera.dart -typedef enum { - FocusModeAuto, - FocusModeLocked, -} FocusMode; - -static NSString *getStringForFocusMode(FocusMode mode) { - switch (mode) { - case FocusModeAuto: - return @"auto"; - case FocusModeLocked: - return @"locked"; - } - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat:@"Unknown string for focus mode"] - }]; - @throw error; -} - -static FocusMode getFocusModeForString(NSString *mode) { - if ([mode isEqualToString:@"auto"]) { - return FocusModeAuto; - } else if ([mode isEqualToString:@"locked"]) { - return FocusModeLocked; - } else { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat:@"Unknown focus mode %@", mode] - }]; - @throw error; - } -} - -// Mirrors ResolutionPreset in camera.dart -typedef enum { - veryLow, - low, - medium, - high, - veryHigh, - ultraHigh, - max, -} ResolutionPreset; - -static ResolutionPreset getResolutionPresetForString(NSString *preset) { - if ([preset isEqualToString:@"veryLow"]) { - return veryLow; - } else if ([preset isEqualToString:@"low"]) { - return low; - } else if ([preset isEqualToString:@"medium"]) { - return medium; - } else if ([preset isEqualToString:@"high"]) { - return high; - } else if ([preset isEqualToString:@"veryHigh"]) { - return veryHigh; - } else if ([preset isEqualToString:@"ultraHigh"]) { - return ultraHigh; - } else if ([preset isEqualToString:@"max"]) { - return max; - } else { - NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : [NSString - stringWithFormat:@"Unknown resolution preset %@", preset] - }]; - @throw error; - } -} - -@interface FLTCam : NSObject -@property(readonly, nonatomic) int64_t textureId; -@property(nonatomic, copy) void (^onFrameAvailable)(void); -@property BOOL enableAudio; -@property(nonatomic) FLTImageStreamHandler *imageStreamHandler; -@property(nonatomic) FlutterMethodChannel *methodChannel; -@property(readonly, nonatomic) AVCaptureSession *captureSession; -@property(readonly, nonatomic) AVCaptureDevice *captureDevice; -@property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); -@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput; -@property(readonly, nonatomic) AVCaptureInput *captureVideoInput; -@property(readonly) CVPixelBufferRef volatile latestPixelBuffer; -@property(readonly, nonatomic) CGSize previewSize; -@property(readonly, nonatomic) CGSize captureSize; -@property(strong, nonatomic) AVAssetWriter *videoWriter; -@property(strong, nonatomic) AVAssetWriterInput *videoWriterInput; -@property(strong, nonatomic) AVAssetWriterInput *audioWriterInput; -@property(strong, nonatomic) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor; -@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput; -@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput; -@property(strong, nonatomic) NSString *videoRecordingPath; -@property(assign, nonatomic) BOOL isRecording; -@property(assign, nonatomic) BOOL isRecordingPaused; -@property(assign, nonatomic) BOOL videoIsDisconnected; -@property(assign, nonatomic) BOOL audioIsDisconnected; -@property(assign, nonatomic) BOOL isAudioSetup; -@property(assign, nonatomic) BOOL isStreamingImages; -@property(assign, nonatomic) BOOL isPreviewPaused; -@property(assign, nonatomic) ResolutionPreset resolutionPreset; -@property(assign, nonatomic) ExposureMode exposureMode; -@property(assign, nonatomic) FocusMode focusMode; -@property(assign, nonatomic) FlashMode flashMode; -@property(assign, nonatomic) UIDeviceOrientation lockedCaptureOrientation; -@property(assign, nonatomic) CMTime lastVideoSampleTime; -@property(assign, nonatomic) CMTime lastAudioSampleTime; -@property(assign, nonatomic) CMTime videoTimeOffset; -@property(assign, nonatomic) CMTime audioTimeOffset; -@property(nonatomic) CMMotionManager *motionManager; -@property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor; -@end - -@implementation FLTCam { - dispatch_queue_t _dispatchQueue; - UIDeviceOrientation _deviceOrientation; -} -// Format used for video and image streaming. -FourCharCode videoFormat = kCVPixelFormatType_32BGRA; -NSString *const errorMethod = @"error"; - -- (instancetype)initWithCameraName:(NSString *)cameraName - resolutionPreset:(NSString *)resolutionPreset - enableAudio:(BOOL)enableAudio - orientation:(UIDeviceOrientation)orientation - dispatchQueue:(dispatch_queue_t)dispatchQueue - error:(NSError **)error { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - @try { - _resolutionPreset = getResolutionPresetForString(resolutionPreset); - } @catch (NSError *e) { - *error = e; - } - _enableAudio = enableAudio; - _dispatchQueue = dispatchQueue; - _captureSession = [[AVCaptureSession alloc] init]; - _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName]; - _flashMode = _captureDevice.hasFlash ? FlashModeAuto : FlashModeOff; - _exposureMode = ExposureModeAuto; - _focusMode = FocusModeAuto; - _lockedCaptureOrientation = UIDeviceOrientationUnknown; - _deviceOrientation = orientation; - - NSError *localError = nil; - _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice - error:&localError]; - - if (localError) { - *error = localError; - return nil; - } - - _captureVideoOutput = [AVCaptureVideoDataOutput new]; - _captureVideoOutput.videoSettings = - @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)}; - [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES]; - [_captureVideoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()]; - - AVCaptureConnection *connection = - [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports - output:_captureVideoOutput]; - - if ([_captureDevice position] == AVCaptureDevicePositionFront) { - connection.videoMirrored = YES; - } - - [_captureSession addInputWithNoConnections:_captureVideoInput]; - [_captureSession addOutputWithNoConnections:_captureVideoOutput]; - [_captureSession addConnection:connection]; - - if (@available(iOS 10.0, *)) { - _capturePhotoOutput = [AVCapturePhotoOutput new]; - [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; - [_captureSession addOutput:_capturePhotoOutput]; - } - _motionManager = [[CMMotionManager alloc] init]; - [_motionManager startAccelerometerUpdates]; - - [self setCaptureSessionPreset:_resolutionPreset]; - [self updateOrientation]; - - return self; -} - -- (void)start { - [_captureSession startRunning]; -} - -- (void)stop { - [_captureSession stopRunning]; -} - -- (void)setDeviceOrientation:(UIDeviceOrientation)orientation { - if (_deviceOrientation == orientation) { - return; - } - - _deviceOrientation = orientation; - [self updateOrientation]; -} - -- (void)updateOrientation { - if (_isRecording) { - return; - } - - UIDeviceOrientation orientation = (_lockedCaptureOrientation != UIDeviceOrientationUnknown) - ? _lockedCaptureOrientation - : _deviceOrientation; - - [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput]; - [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput]; -} - -- (void)updateOrientation:(UIDeviceOrientation)orientation - forCaptureOutput:(AVCaptureOutput *)captureOutput { - if (!captureOutput) { - return; - } - - AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo]; - if (connection && connection.isVideoOrientationSupported) { - connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation]; - } -} - -- (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { - AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; - if (_resolutionPreset == max) { - [settings setHighResolutionPhotoEnabled:YES]; - } - - AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(_flashMode); - if (avFlashMode != -1) { - [settings setFlashMode:avFlashMode]; - } - NSError *error; - NSString *path = [self getTemporaryFilePathWithExtension:@"jpg" - subfolder:@"pictures" - prefix:@"CAP_" - error:error]; - if (error) { - result(getFlutterError(error)); - return; - } - - [_capturePhotoOutput capturePhotoWithSettings:settings - delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path - result:result]]; -} - -- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation: - (UIDeviceOrientation)deviceOrientation { - if (deviceOrientation == UIDeviceOrientationPortrait) { - return AVCaptureVideoOrientationPortrait; - } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) { - // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation - // is landscape left the video orientation should be landscape right. - return AVCaptureVideoOrientationLandscapeRight; - } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) { - // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation - // is landscape right the video orientation should be landscape left. - return AVCaptureVideoOrientationLandscapeLeft; - } else if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) { - return AVCaptureVideoOrientationPortraitUpsideDown; - } else { - return AVCaptureVideoOrientationPortrait; - } -} - -- (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension - subfolder:(NSString *)subfolder - prefix:(NSString *)prefix - error:(NSError *)error { - NSString *docDir = - NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; - NSString *fileDir = - [[docDir stringByAppendingPathComponent:@"camera"] stringByAppendingPathComponent:subfolder]; - NSString *fileName = [prefix stringByAppendingString:[[NSUUID UUID] UUIDString]]; - NSString *file = - [[fileDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:extension]; - - NSFileManager *fm = [NSFileManager defaultManager]; - if (![fm fileExistsAtPath:fileDir]) { - [[NSFileManager defaultManager] createDirectoryAtPath:fileDir - withIntermediateDirectories:true - attributes:nil - error:&error]; - if (error) { - return nil; - } - } - - return file; -} - -- (void)setCaptureSessionPreset:(ResolutionPreset)resolutionPreset { - switch (resolutionPreset) { - case max: - case ultraHigh: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { - _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; - _previewSize = CGSizeMake(3840, 2160); - break; - } - if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) { - _captureSession.sessionPreset = AVCaptureSessionPresetHigh; - _previewSize = - CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width, - _captureDevice.activeFormat.highResolutionStillImageDimensions.height); - break; - } - case veryHigh: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) { - _captureSession.sessionPreset = AVCaptureSessionPreset1920x1080; - _previewSize = CGSizeMake(1920, 1080); - break; - } - case high: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) { - _captureSession.sessionPreset = AVCaptureSessionPreset1280x720; - _previewSize = CGSizeMake(1280, 720); - break; - } - case medium: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) { - _captureSession.sessionPreset = AVCaptureSessionPreset640x480; - _previewSize = CGSizeMake(640, 480); - break; - } - case low: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) { - _captureSession.sessionPreset = AVCaptureSessionPreset352x288; - _previewSize = CGSizeMake(352, 288); - break; - } - default: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetLow]) { - _captureSession.sessionPreset = AVCaptureSessionPresetLow; - _previewSize = CGSizeMake(352, 288); - } else { - NSError *error = - [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorUnknown - userInfo:@{ - NSLocalizedDescriptionKey : - @"No capture session available for current capture session." - }]; - @throw error; - } - } -} - -- (void)captureOutput:(AVCaptureOutput *)output - didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer - fromConnection:(AVCaptureConnection *)connection { - if (output == _captureVideoOutput) { - CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CFRetain(newBuffer); - CVPixelBufferRef old = _latestPixelBuffer; - while (!OSAtomicCompareAndSwapPtrBarrier(old, newBuffer, (void **)&_latestPixelBuffer)) { - old = _latestPixelBuffer; - } - if (old != nil) { - CFRelease(old); - } - if (_onFrameAvailable) { - _onFrameAvailable(); - } - } - if (!CMSampleBufferDataIsReady(sampleBuffer)) { - [_methodChannel invokeMethod:errorMethod - arguments:@"sample buffer is not ready. Skipping sample"]; - return; - } - if (_isStreamingImages) { - if (_imageStreamHandler.eventSink) { - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - - size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer); - size_t imageHeight = CVPixelBufferGetHeight(pixelBuffer); - - NSMutableArray *planes = [NSMutableArray array]; - - const Boolean isPlanar = CVPixelBufferIsPlanar(pixelBuffer); - size_t planeCount; - if (isPlanar) { - planeCount = CVPixelBufferGetPlaneCount(pixelBuffer); - } else { - planeCount = 1; - } - - for (int i = 0; i < planeCount; i++) { - void *planeAddress; - size_t bytesPerRow; - size_t height; - size_t width; - - if (isPlanar) { - planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i); - bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i); - height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i); - width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i); - } else { - planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer); - bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer); - height = CVPixelBufferGetHeight(pixelBuffer); - width = CVPixelBufferGetWidth(pixelBuffer); - } - - NSNumber *length = @(bytesPerRow * height); - NSData *bytes = [NSData dataWithBytes:planeAddress length:length.unsignedIntegerValue]; - - NSMutableDictionary *planeBuffer = [NSMutableDictionary dictionary]; - planeBuffer[@"bytesPerRow"] = @(bytesPerRow); - planeBuffer[@"width"] = @(width); - planeBuffer[@"height"] = @(height); - planeBuffer[@"bytes"] = [FlutterStandardTypedData typedDataWithBytes:bytes]; - - [planes addObject:planeBuffer]; - } - - NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary]; - imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth]; - imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight]; - imageBuffer[@"format"] = @(videoFormat); - imageBuffer[@"planes"] = planes; - imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]]; - Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]); - Float64 nsExposureDuration = 1000000000 * exposureDuration; - imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration]; - imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]]; - - _imageStreamHandler.eventSink(imageBuffer); - - CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - } - } - if (_isRecording && !_isRecordingPaused) { - if (_videoWriter.status == AVAssetWriterStatusFailed) { - [_methodChannel invokeMethod:errorMethod - arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; - return; - } - - CFRetain(sampleBuffer); - CMTime currentSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); - - if (_videoWriter.status != AVAssetWriterStatusWriting) { - [_videoWriter startWriting]; - [_videoWriter startSessionAtSourceTime:currentSampleTime]; - } - - if (output == _captureVideoOutput) { - if (_videoIsDisconnected) { - _videoIsDisconnected = NO; - - if (_videoTimeOffset.value == 0) { - _videoTimeOffset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); - } else { - CMTime offset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); - _videoTimeOffset = CMTimeAdd(_videoTimeOffset, offset); - } - - return; - } - - _lastVideoSampleTime = currentSampleTime; - - CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); - CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset); - [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime]; - } else { - CMTime dur = CMSampleBufferGetDuration(sampleBuffer); - - if (dur.value > 0) { - currentSampleTime = CMTimeAdd(currentSampleTime, dur); - } - - if (_audioIsDisconnected) { - _audioIsDisconnected = NO; - - if (_audioTimeOffset.value == 0) { - _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); - } else { - CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); - _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset); - } - - return; - } - - _lastAudioSampleTime = currentSampleTime; - - if (_audioTimeOffset.value != 0) { - CFRelease(sampleBuffer); - sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset]; - } - - [self newAudioSample:sampleBuffer]; - } - - CFRelease(sampleBuffer); - } -} - -- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset CF_RETURNS_RETAINED { - CMItemCount count; - CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); - CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count); - CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); - for (CMItemCount i = 0; i < count; i++) { - pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); - pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); - } - CMSampleBufferRef sout; - CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); - free(pInfo); - return sout; -} - -- (void)newVideoSample:(CMSampleBufferRef)sampleBuffer { - if (_videoWriter.status != AVAssetWriterStatusWriting) { - if (_videoWriter.status == AVAssetWriterStatusFailed) { - [_methodChannel invokeMethod:errorMethod - arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; - } - return; - } - if (_videoWriterInput.readyForMoreMediaData) { - if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) { - [_methodChannel - invokeMethod:errorMethod - arguments:[NSString stringWithFormat:@"%@", @"Unable to write to video input"]]; - } - } -} - -- (void)newAudioSample:(CMSampleBufferRef)sampleBuffer { - if (_videoWriter.status != AVAssetWriterStatusWriting) { - if (_videoWriter.status == AVAssetWriterStatusFailed) { - [_methodChannel invokeMethod:errorMethod - arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; - } - return; - } - if (_audioWriterInput.readyForMoreMediaData) { - if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) { - [_methodChannel - invokeMethod:errorMethod - arguments:[NSString stringWithFormat:@"%@", @"Unable to write to audio input"]]; - } - } -} - -- (void)close { - [_captureSession stopRunning]; - for (AVCaptureInput *input in [_captureSession inputs]) { - [_captureSession removeInput:input]; - } - for (AVCaptureOutput *output in [_captureSession outputs]) { - [_captureSession removeOutput:output]; - } -} - -- (void)dealloc { - if (_latestPixelBuffer) { - CFRelease(_latestPixelBuffer); - } - [_motionManager stopAccelerometerUpdates]; -} - -- (CVPixelBufferRef)copyPixelBuffer { - CVPixelBufferRef pixelBuffer = _latestPixelBuffer; - while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) { - pixelBuffer = _latestPixelBuffer; - } - - return pixelBuffer; -} - -- (void)startVideoRecordingWithResult:(FlutterResult)result { - if (!_isRecording) { - NSError *error; - _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" - subfolder:@"videos" - prefix:@"REC_" - error:error]; - if (error) { - result(getFlutterError(error)); - return; - } - if (![self setupWriterForPath:_videoRecordingPath]) { - result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]); - return; - } - _isRecording = YES; - _isRecordingPaused = NO; - _videoTimeOffset = CMTimeMake(0, 1); - _audioTimeOffset = CMTimeMake(0, 1); - _videoIsDisconnected = NO; - _audioIsDisconnected = NO; - result(nil); - } else { - result([FlutterError errorWithCode:@"Error" message:@"Video is already recording" details:nil]); - } -} - -- (void)stopVideoRecordingWithResult:(FlutterResult)result { - if (_isRecording) { - _isRecording = NO; - - if (_videoWriter.status != AVAssetWriterStatusUnknown) { - [_videoWriter finishWritingWithCompletionHandler:^{ - if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { - [self updateOrientation]; - result(self->_videoRecordingPath); - self->_videoRecordingPath = nil; - } else { - result([FlutterError errorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]); - } - }]; - } - } else { - NSError *error = - [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorResourceUnavailable - userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - result(getFlutterError(error)); - } -} - -- (void)pauseVideoRecordingWithResult:(FlutterResult)result { - _isRecordingPaused = YES; - _videoIsDisconnected = YES; - _audioIsDisconnected = YES; - result(nil); -} - -- (void)resumeVideoRecordingWithResult:(FlutterResult)result { - _isRecordingPaused = NO; - result(nil); -} - -- (void)lockCaptureOrientationWithResult:(FlutterResult)result - orientation:(NSString *)orientationStr { - UIDeviceOrientation orientation; - @try { - orientation = getUIDeviceOrientationForString(orientationStr); - } @catch (NSError *e) { - result(getFlutterError(e)); - return; - } - - if (_lockedCaptureOrientation != orientation) { - _lockedCaptureOrientation = orientation; - [self updateOrientation]; - } - - result(nil); -} - -- (void)unlockCaptureOrientationWithResult:(FlutterResult)result { - _lockedCaptureOrientation = UIDeviceOrientationUnknown; - [self updateOrientation]; - result(nil); -} - -- (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { - FlashMode mode; - @try { - mode = getFlashModeForString(modeStr); - } @catch (NSError *e) { - result(getFlutterError(e)); - return; - } - if (mode == FlashModeTorch) { - if (!_captureDevice.hasTorch) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support torch mode" - details:nil]); - return; - } - if (!_captureDevice.isTorchAvailable) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Torch mode is currently not available" - details:nil]); - return; - } - if (_captureDevice.torchMode != AVCaptureTorchModeOn) { - [_captureDevice lockForConfiguration:nil]; - [_captureDevice setTorchMode:AVCaptureTorchModeOn]; - [_captureDevice unlockForConfiguration]; - } - } else { - if (!_captureDevice.hasFlash) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not have flash capabilities" - details:nil]); - return; - } - AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); - if (![_capturePhotoOutput.supportedFlashModes - containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { - result([FlutterError errorWithCode:@"setFlashModeFailed" - message:@"Device does not support this specific flash mode" - details:nil]); - return; - } - if (_captureDevice.torchMode != AVCaptureTorchModeOff) { - [_captureDevice lockForConfiguration:nil]; - [_captureDevice setTorchMode:AVCaptureTorchModeOff]; - [_captureDevice unlockForConfiguration]; - } - } - _flashMode = mode; - result(nil); -} - -- (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { - ExposureMode mode; - @try { - mode = getExposureModeForString(modeStr); - } @catch (NSError *e) { - result(getFlutterError(e)); - return; - } - _exposureMode = mode; - [self applyExposureMode]; - result(nil); -} - -- (void)applyExposureMode { - [_captureDevice lockForConfiguration:nil]; - switch (_exposureMode) { - case ExposureModeLocked: - [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; - break; - case ExposureModeAuto: - if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { - [_captureDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure]; - } else { - [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; - } - break; - } - [_captureDevice unlockForConfiguration]; -} - -- (void)setFocusModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { - FocusMode mode; - @try { - mode = getFocusModeForString(modeStr); - } @catch (NSError *e) { - result(getFlutterError(e)); - return; - } - _focusMode = mode; - [self applyFocusMode]; - result(nil); -} - -- (void)applyFocusMode { - [self applyFocusMode:_focusMode onDevice:_captureDevice]; -} - -/** - * Applies FocusMode on the AVCaptureDevice. - * - * If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use - * AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to - * AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor - * AVCaptureFocusModeAutoFocus are supported focus mode will not be set. - * If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use - * AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not - * be set. - * - * @param focusMode The focus mode that should be applied to the @captureDevice instance. - * @param captureDevice The AVCaptureDevice to which the @focusMode will be applied. - */ -- (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice { - [captureDevice lockForConfiguration:nil]; - switch (focusMode) { - case FocusModeLocked: - if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { - [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; - } - break; - case FocusModeAuto: - if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { - [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; - } else if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { - [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; - } - break; - } - [captureDevice unlockForConfiguration]; -} - -- (void)pausePreviewWithResult:(FlutterResult)result { - _isPreviewPaused = true; - result(nil); -} - -- (void)resumePreviewWithResult:(FlutterResult)result { - _isPreviewPaused = false; - result(nil); -} - -- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation - x:(double)x - y:(double)y { - double oldX = x, oldY = y; - switch (orientation) { - case UIDeviceOrientationPortrait: // 90 ccw - y = 1 - oldX; - x = oldY; - break; - case UIDeviceOrientationPortraitUpsideDown: // 90 cw - x = 1 - oldY; - y = oldX; - break; - case UIDeviceOrientationLandscapeRight: // 180 - x = 1 - x; - y = 1 - y; - break; - case UIDeviceOrientationLandscapeLeft: - default: - // No rotation required - break; - } - return CGPointMake(x, y); -} - -- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { - if (!_captureDevice.isExposurePointOfInterestSupported) { - result([FlutterError errorWithCode:@"setExposurePointFailed" - message:@"Device does not have exposure point capabilities" - details:nil]); - return; - } - UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; - [_captureDevice lockForConfiguration:nil]; - [_captureDevice setExposurePointOfInterest:[self getCGPointForCoordsWithOrientation:orientation - x:x - y:y]]; - [_captureDevice unlockForConfiguration]; - // Retrigger auto exposure - [self applyExposureMode]; - result(nil); -} - -- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { - if (!_captureDevice.isFocusPointOfInterestSupported) { - result([FlutterError errorWithCode:@"setFocusPointFailed" - message:@"Device does not have focus point capabilities" - details:nil]); - return; - } - UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; - [_captureDevice lockForConfiguration:nil]; - - [_captureDevice setFocusPointOfInterest:[self getCGPointForCoordsWithOrientation:orientation - x:x - y:y]]; - [_captureDevice unlockForConfiguration]; - // Retrigger auto focus - [self applyFocusMode]; - - result(nil); -} - -- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { - [_captureDevice lockForConfiguration:nil]; - [_captureDevice setExposureTargetBias:offset completionHandler:nil]; - [_captureDevice unlockForConfiguration]; - result(@(offset)); -} - -- (void)startImageStreamWithMessenger:(NSObject *)messenger { - if (!_isStreamingImages) { - FlutterEventChannel *eventChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" - binaryMessenger:messenger]; - - _imageStreamHandler = [[FLTImageStreamHandler alloc] init]; - [eventChannel setStreamHandler:_imageStreamHandler]; - - _isStreamingImages = YES; - } else { - [_methodChannel invokeMethod:errorMethod - arguments:@"Images from camera are already streaming!"]; - } -} - -- (void)stopImageStream { - if (_isStreamingImages) { - _isStreamingImages = NO; - _imageStreamHandler = nil; - } else { - [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"]; - } -} - -- (void)getMaxZoomLevelWithResult:(FlutterResult)result { - CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; - - result([NSNumber numberWithFloat:maxZoomFactor]); -} - -- (void)getMinZoomLevelWithResult:(FlutterResult)result { - CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; - - result([NSNumber numberWithFloat:minZoomFactor]); -} - -- (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { - CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; - CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; - - if (maxAvailableZoomFactor < zoom || minAvailableZoomFactor > zoom) { - NSString *errorMessage = [NSString - stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", - minAvailableZoomFactor, maxAvailableZoomFactor]; - FlutterError *error = [FlutterError errorWithCode:@"ZOOM_ERROR" - message:errorMessage - details:nil]; - result(error); - return; - } - - NSError *error = nil; - if (![_captureDevice lockForConfiguration:&error]) { - result(getFlutterError(error)); - return; - } - _captureDevice.videoZoomFactor = zoom; - [_captureDevice unlockForConfiguration]; - - result(nil); -} - -- (CGFloat)getMinAvailableZoomFactor { - if (@available(iOS 11.0, *)) { - return _captureDevice.minAvailableVideoZoomFactor; - } else { - return 1.0; - } -} - -- (CGFloat)getMaxAvailableZoomFactor { - if (@available(iOS 11.0, *)) { - return _captureDevice.maxAvailableVideoZoomFactor; - } else { - return _captureDevice.activeFormat.videoMaxZoomFactor; - } -} - -- (BOOL)setupWriterForPath:(NSString *)path { - NSError *error = nil; - NSURL *outputURL; - if (path != nil) { - outputURL = [NSURL fileURLWithPath:path]; - } else { - return NO; - } - if (_enableAudio && !_isAudioSetup) { - [self setUpCaptureSessionForAudio]; - } - - _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL - fileType:AVFileTypeMPEG4 - error:&error]; - NSParameterAssert(_videoWriter); - if (error) { - [_methodChannel invokeMethod:errorMethod arguments:error.description]; - return NO; - } - - NSDictionary *videoSettings = [_captureVideoOutput - recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4]; - _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo - outputSettings:videoSettings]; - - _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor - assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput - sourcePixelBufferAttributes:@{ - (NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat) - }]; - - NSParameterAssert(_videoWriterInput); - - _videoWriterInput.expectsMediaDataInRealTime = YES; - - // Add the audio input - if (_enableAudio) { - AudioChannelLayout acl; - bzero(&acl, sizeof(acl)); - acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; - NSDictionary *audioOutputSettings = nil; - // Both type of audio inputs causes output video file to be corrupted. - audioOutputSettings = @{ - AVFormatIDKey : [NSNumber numberWithInt:kAudioFormatMPEG4AAC], - AVSampleRateKey : [NSNumber numberWithFloat:44100.0], - AVNumberOfChannelsKey : [NSNumber numberWithInt:1], - AVChannelLayoutKey : [NSData dataWithBytes:&acl length:sizeof(acl)], - }; - _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio - outputSettings:audioOutputSettings]; - _audioWriterInput.expectsMediaDataInRealTime = YES; - - [_videoWriter addInput:_audioWriterInput]; - [_audioOutput setSampleBufferDelegate:self queue:_dispatchQueue]; - } - - if (_flashMode == FlashModeTorch) { - [self.captureDevice lockForConfiguration:nil]; - [self.captureDevice setTorchMode:AVCaptureTorchModeOn]; - [self.captureDevice unlockForConfiguration]; - } - - [_videoWriter addInput:_videoWriterInput]; - - [_captureVideoOutput setSampleBufferDelegate:self queue:_dispatchQueue]; - - return YES; -} - -- (void)setUpCaptureSessionForAudio { - NSError *error = nil; - // Create a device input with the device and add it to the session. - // Setup the audio input. - AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; - AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice - error:&error]; - if (error) { - [_methodChannel invokeMethod:errorMethod arguments:error.description]; - } - // Setup the audio output. - _audioOutput = [[AVCaptureAudioDataOutput alloc] init]; - - if ([_captureSession canAddInput:audioInput]) { - [_captureSession addInput:audioInput]; - - if ([_captureSession canAddOutput:_audioOutput]) { - [_captureSession addOutput:_audioOutput]; - _isAudioSetup = YES; - } else { - [_methodChannel invokeMethod:errorMethod - arguments:@"Unable to add Audio input/output to session capture"]; - _isAudioSetup = NO; - } - } -} -@end - -@interface CameraPlugin () -@property(readonly, nonatomic) NSObject *registry; -@property(readonly, nonatomic) NSObject *messenger; -@property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; -@end - -@implementation CameraPlugin { - dispatch_queue_t _dispatchQueue; -} -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera" - binaryMessenger:[registrar messenger]]; - CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures] - messenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (instancetype)initWithRegistry:(NSObject *)registry - messenger:(NSObject *)messenger { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _registry = registry; - _messenger = messenger; - [self initDeviceEventMethodChannel]; - [self startOrientationListener]; - return self; -} - -- (void)initDeviceEventMethodChannel { - _deviceEventMethodChannel = - [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" - binaryMessenger:_messenger]; -} - -- (void)startOrientationListener { - [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(orientationChanged:) - name:UIDeviceOrientationDidChangeNotification - object:[UIDevice currentDevice]]; -} - -- (void)orientationChanged:(NSNotification *)note { - UIDevice *device = note.object; - UIDeviceOrientation orientation = device.orientation; - - if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown) { - // Do not change when oriented flat. - return; - } - - if (_camera) { - [_camera setDeviceOrientation:orientation]; - } - - [self sendDeviceOrientation:orientation]; -} - -- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { - [_deviceEventMethodChannel - invokeMethod:@"orientation_changed" - arguments:@{@"orientation" : getStringForUIDeviceOrientation(orientation)}]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (_dispatchQueue == nil) { - _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); - } - - // Invoke the plugin on another dispatch queue to avoid blocking the UI. - dispatch_async(_dispatchQueue, ^{ - [self handleMethodCallAsync:call result:result]; - }); -} - -- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"availableCameras" isEqualToString:call.method]) { - if (@available(iOS 10.0, *)) { - AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession - discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeBuiltInWideAngleCamera ] - mediaType:AVMediaTypeVideo - position:AVCaptureDevicePositionUnspecified]; - NSArray *devices = discoverySession.devices; - NSMutableArray *> *reply = - [[NSMutableArray alloc] initWithCapacity:devices.count]; - for (AVCaptureDevice *device in devices) { - NSString *lensFacing; - switch ([device position]) { - case AVCaptureDevicePositionBack: - lensFacing = @"back"; - break; - case AVCaptureDevicePositionFront: - lensFacing = @"front"; - break; - case AVCaptureDevicePositionUnspecified: - lensFacing = @"external"; - break; - } - [reply addObject:@{ - @"name" : [device uniqueID], - @"lensFacing" : lensFacing, - @"sensorOrientation" : @90, - }]; - } - result(reply); - } else { - result(FlutterMethodNotImplemented); - } - } 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] - dispatchQueue:_dispatchQueue - error:&error]; - - if (error) { - result(getFlutterError(error)); - } else { - if (_camera) { - [_camera close]; - } - int64_t textureId = [_registry registerTexture:cam]; - _camera = cam; - - result(@{ - @"cameraId" : @(textureId), - }); - } - } else if ([@"startImageStream" isEqualToString:call.method]) { - [_camera startImageStreamWithMessenger:_messenger]; - result(nil); - } else if ([@"stopImageStream" isEqualToString:call.method]) { - [_camera stopImageStream]; - result(nil); - } else { - NSDictionary *argsMap = call.arguments; - NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; - if ([@"initialize" isEqualToString:call.method]) { - NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]); - videoFormat = getVideoFormatFromString(videoFormatValue); - - __weak CameraPlugin *weakSelf = self; - _camera.onFrameAvailable = ^{ - if (![weakSelf.camera isPreviewPaused]) { - [weakSelf.registry textureFrameAvailable:cameraId]; - } - }; - FlutterMethodChannel *methodChannel = [FlutterMethodChannel - methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", - (unsigned long)cameraId] - binaryMessenger:_messenger]; - _camera.methodChannel = methodChannel; - [methodChannel - invokeMethod:@"initialized" - arguments:@{ - @"previewWidth" : @(_camera.previewSize.width), - @"previewHeight" : @(_camera.previewSize.height), - @"exposureMode" : getStringForExposureMode([_camera exposureMode]), - @"focusMode" : getStringForFocusMode([_camera focusMode]), - @"exposurePointSupported" : - @([_camera.captureDevice isExposurePointOfInterestSupported]), - @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), - }]; - [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; - [_camera start]; - result(nil); - } else if ([@"takePicture" isEqualToString:call.method]) { - if (@available(iOS 10.0, *)) { - [_camera captureToFile:result]; - } else { - result(FlutterMethodNotImplemented); - } - } else if ([@"dispose" isEqualToString:call.method]) { - [_registry unregisterTexture:cameraId]; - [_camera close]; - _dispatchQueue = nil; - result(nil); - } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { - [_camera setUpCaptureSessionForAudio]; - result(nil); - } else if ([@"startVideoRecording" isEqualToString:call.method]) { - [_camera startVideoRecordingWithResult:result]; - } else if ([@"stopVideoRecording" isEqualToString:call.method]) { - [_camera stopVideoRecordingWithResult:result]; - } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { - [_camera pauseVideoRecordingWithResult:result]; - } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { - [_camera resumeVideoRecordingWithResult:result]; - } else if ([@"getMaxZoomLevel" isEqualToString:call.method]) { - [_camera getMaxZoomLevelWithResult:result]; - } else if ([@"getMinZoomLevel" isEqualToString:call.method]) { - [_camera getMinZoomLevelWithResult:result]; - } else if ([@"setZoomLevel" isEqualToString:call.method]) { - CGFloat zoom = ((NSNumber *)argsMap[@"zoom"]).floatValue; - [_camera setZoomLevel:zoom Result:result]; - } else if ([@"setFlashMode" isEqualToString:call.method]) { - [_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]]; - } else if ([@"setExposureMode" isEqualToString:call.method]) { - [_camera setExposureModeWithResult:result mode:call.arguments[@"mode"]]; - } else if ([@"setExposurePoint" isEqualToString:call.method]) { - BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; - double x = 0.5; - double y = 0.5; - if (!reset) { - x = ((NSNumber *)call.arguments[@"x"]).doubleValue; - y = ((NSNumber *)call.arguments[@"y"]).doubleValue; - } - [_camera setExposurePointWithResult:result x:x y:y]; - } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.minExposureTargetBias)); - } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { - result(@(_camera.captureDevice.maxExposureTargetBias)); - } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { - result(@(0.0)); - } else if ([@"setExposureOffset" isEqualToString:call.method]) { - [_camera setExposureOffsetWithResult:result - offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; - } else if ([@"lockCaptureOrientation" isEqualToString:call.method]) { - [_camera lockCaptureOrientationWithResult:result orientation:call.arguments[@"orientation"]]; - } else if ([@"unlockCaptureOrientation" isEqualToString:call.method]) { - [_camera unlockCaptureOrientationWithResult:result]; - } else if ([@"setFocusMode" isEqualToString:call.method]) { - [_camera setFocusModeWithResult:result mode:call.arguments[@"mode"]]; - } else if ([@"setFocusPoint" isEqualToString:call.method]) { - BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; - double x = 0.5; - double y = 0.5; - if (!reset) { - x = ((NSNumber *)call.arguments[@"x"]).doubleValue; - y = ((NSNumber *)call.arguments[@"y"]).doubleValue; - } - [_camera setFocusPointWithResult:result x:x y:y]; - } else if ([@"pausePreview" isEqualToString:call.method]) { - [_camera pausePreviewWithResult:result]; - } else if ([@"resumePreview" isEqualToString:call.method]) { - [_camera resumePreviewWithResult:result]; - } else { - result(FlutterMethodNotImplemented); - } - } -} - -@end diff --git a/packages/camera/camera/ios/camera.podspec b/packages/camera/camera/ios/camera.podspec deleted file mode 100644 index 4a142bd4589a..000000000000 --- a/packages/camera/camera/ios/camera.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'camera' - s.version = '0.0.1' - s.summary = 'Flutter Camera' - s.description = <<-DESC -A Flutter plugin to use the camera from your Flutter app. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/camera' } - s.documentation_url = 'https://pub.dev/packages/camera' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 1e24efbd3dc6..900c2633a5d7 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -2,10 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/camera_controller.dart'; -export 'src/camera_image.dart'; -export 'src/camera_preview.dart'; - export 'package:camera_platform_interface/camera_platform_interface.dart' show CameraDescription, @@ -17,3 +13,7 @@ export 'package:camera_platform_interface/camera_platform_interface.dart' ResolutionPreset, XFile, ImageFormatGroup; + +export 'src/camera_controller.dart'; +export 'src/camera_image.dart'; +export 'src/camera_preview.dart'; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 8cf1e90e36c1..6566e2abc883 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -10,15 +10,14 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:quiver/core.dart'; -final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); - /// Signature for a callback receiving the a camera image. /// /// This is used by [CameraController.startImageStream]. -// ignore: inference_failure_on_function_return_type +// TODO(stuartmorgan): Fix this naming the next time there's a breaking change +// to this package. +// ignore: camel_case_types typedef onLatestImageAvailable = Function(CameraImage image); /// Completes with a list of available cameras. @@ -28,6 +27,10 @@ Future> availableCameras() async { return CameraPlatform.instance.availableCameras(); } +// TODO(stuartmorgan): Remove this once the package requires 2.10, where the +// dart:async `unawaited` accepts a nullable future. +void _unawaited(Future? future) {} + /// The state of a [CameraController]. class CameraValue { /// Creates a new camera controller state. @@ -192,7 +195,7 @@ class CameraValue { @override String toString() { - return '$runtimeType(' + return '${objectRuntimeType(this, 'CameraValue')}(' 'isRecordingVideo: $isRecordingVideo, ' 'isInitialized: $isInitialized, ' 'errorDescription: $errorDescription, ' @@ -252,9 +255,10 @@ class CameraController extends ValueNotifier { int _cameraId = kUninitializedCameraId; bool _isDisposed = false; - StreamSubscription? _imageStreamSubscription; + StreamSubscription? _imageStreamSubscription; FutureOr? _initCalled; - StreamSubscription? _deviceOrientationSubscription; + StreamSubscription? + _deviceOrientationSubscription; /// Checks whether [CameraController.dispose] has completed successfully. /// @@ -277,10 +281,12 @@ class CameraController extends ValueNotifier { ); } try { - Completer _initializeCompleter = Completer(); + final Completer _initializeCompleter = + Completer(); - _deviceOrientationSubscription = - CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + _deviceOrientationSubscription = CameraPlatform.instance + .onDeviceOrientationChanged() + .listen((DeviceOrientationChangedEvent event) { value = value.copyWith( deviceOrientation: event.orientation, ); @@ -292,10 +298,10 @@ class CameraController extends ValueNotifier { enableAudio: enableAudio, ); - unawaited(CameraPlatform.instance + _unawaited(CameraPlatform.instance .onCameraInitialized(_cameraId) .first - .then((event) { + .then((CameraInitializedEvent event) { _initializeCompleter.complete(event); })); @@ -312,13 +318,13 @@ class CameraController extends ValueNotifier { event.previewHeight, )), exposureMode: await _initializeCompleter.future - .then((event) => event.exposureMode), - focusMode: - await _initializeCompleter.future.then((event) => event.focusMode), - exposurePointSupported: await _initializeCompleter.future - .then((event) => event.exposurePointSupported), + .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((event) => event.focusPointSupported), + .then((CameraInitializedEvent event) => event.focusPointSupported), ); } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -351,7 +357,8 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.pausePreview(_cameraId); value = value.copyWith( isPreviewPaused: true, - previewPauseOrientation: Optional.of(this.value.deviceOrientation)); + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -365,7 +372,8 @@ class CameraController extends ValueNotifier { try { await CameraPlatform.instance.resumePreview(_cameraId); value = value.copyWith( - isPreviewPaused: false, previewPauseOrientation: Optional.absent()); + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -375,7 +383,7 @@ class CameraController extends ValueNotifier { /// /// Throws a [CameraException] if the capture fails. Future takePicture() async { - _throwIfNotInitialized("takePicture"); + _throwIfNotInitialized('takePicture'); if (value.isTakingPicture) { throw CameraException( 'Previous capture has not returned yet.', @@ -384,7 +392,7 @@ class CameraController extends ValueNotifier { } try { value = value.copyWith(isTakingPicture: true); - XFile file = await CameraPlatform.instance.takePicture(_cameraId); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); value = value.copyWith(isTakingPicture: false); return file; } on PlatformException catch (e) { @@ -413,7 +421,7 @@ class CameraController extends ValueNotifier { Future startImageStream(onLatestImageAvailable onAvailable) async { assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); - _throwIfNotInitialized("startImageStream"); + _throwIfNotInitialized('startImageStream'); if (value.isRecordingVideo) { throw CameraException( 'A video recording is already started.', @@ -428,19 +436,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)); - }, - ); } /// Stop streaming images from platform camera. @@ -453,7 +457,7 @@ class CameraController extends ValueNotifier { Future stopImageStream() async { assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); - _throwIfNotInitialized("stopImageStream"); + _throwIfNotInitialized('stopImageStream'); if (value.isRecordingVideo) { throw CameraException( 'A video recording is already started.', @@ -469,13 +473,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. @@ -483,7 +485,7 @@ class CameraController extends ValueNotifier { /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. Future startVideoRecording() async { - _throwIfNotInitialized("startVideoRecording"); + _throwIfNotInitialized('startVideoRecording'); if (value.isRecordingVideo) { throw CameraException( 'A video recording is already started.', @@ -502,7 +504,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); @@ -513,7 +515,7 @@ class CameraController extends ValueNotifier { /// /// Throws a [CameraException] if the capture failed. Future stopVideoRecording() async { - _throwIfNotInitialized("stopVideoRecording"); + _throwIfNotInitialized('stopVideoRecording'); if (!value.isRecordingVideo) { throw CameraException( 'No video is recording', @@ -521,10 +523,11 @@ class CameraController extends ValueNotifier { ); } try { - XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); value = value.copyWith( isRecordingVideo: false, - recordingOrientation: Optional.absent(), + recordingOrientation: const Optional.absent(), ); return file; } on PlatformException catch (e) { @@ -536,7 +539,7 @@ class CameraController extends ValueNotifier { /// /// This feature is only available on iOS and Android sdk 24+. Future pauseVideoRecording() async { - _throwIfNotInitialized("pauseVideoRecording"); + _throwIfNotInitialized('pauseVideoRecording'); if (!value.isRecordingVideo) { throw CameraException( 'No video is recording', @@ -555,7 +558,7 @@ class CameraController extends ValueNotifier { /// /// This feature is only available on iOS and Android sdk 24+. Future resumeVideoRecording() async { - _throwIfNotInitialized("resumeVideoRecording"); + _throwIfNotInitialized('resumeVideoRecording'); if (!value.isRecordingVideo) { throw CameraException( 'No video is recording', @@ -572,7 +575,7 @@ class CameraController extends ValueNotifier { /// Returns a widget showing a live camera preview. Widget buildPreview() { - _throwIfNotInitialized("buildPreview"); + _throwIfNotInitialized('buildPreview'); try { return CameraPlatform.instance.buildPreview(_cameraId); } on PlatformException catch (e) { @@ -582,7 +585,7 @@ class CameraController extends ValueNotifier { /// Gets the maximum supported zoom level for the selected camera. Future getMaxZoomLevel() { - _throwIfNotInitialized("getMaxZoomLevel"); + _throwIfNotInitialized('getMaxZoomLevel'); try { return CameraPlatform.instance.getMaxZoomLevel(_cameraId); } on PlatformException catch (e) { @@ -592,7 +595,7 @@ class CameraController extends ValueNotifier { /// Gets the minimum supported zoom level for the selected camera. Future getMinZoomLevel() { - _throwIfNotInitialized("getMinZoomLevel"); + _throwIfNotInitialized('getMinZoomLevel'); try { return CameraPlatform.instance.getMinZoomLevel(_cameraId); } on PlatformException catch (e) { @@ -606,7 +609,7 @@ class CameraController extends ValueNotifier { /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException` /// when an illegal zoom level is suplied. Future setZoomLevel(double zoom) { - _throwIfNotInitialized("setZoomLevel"); + _throwIfNotInitialized('setZoomLevel'); try { return CameraPlatform.instance.setZoomLevel(_cameraId, zoom); } on PlatformException catch (e) { @@ -662,7 +665,7 @@ class CameraController extends ValueNotifier { /// Gets the minimum supported exposure offset for the selected camera in EV units. Future getMinExposureOffset() async { - _throwIfNotInitialized("getMinExposureOffset"); + _throwIfNotInitialized('getMinExposureOffset'); try { return CameraPlatform.instance.getMinExposureOffset(_cameraId); } on PlatformException catch (e) { @@ -672,7 +675,7 @@ class CameraController extends ValueNotifier { /// Gets the maximum supported exposure offset for the selected camera in EV units. Future getMaxExposureOffset() async { - _throwIfNotInitialized("getMaxExposureOffset"); + _throwIfNotInitialized('getMaxExposureOffset'); try { return CameraPlatform.instance.getMaxExposureOffset(_cameraId); } on PlatformException catch (e) { @@ -684,7 +687,7 @@ class CameraController extends ValueNotifier { /// /// Returns 0 when the camera supports using a free value without stepping. Future getExposureOffsetStepSize() async { - _throwIfNotInitialized("getExposureOffsetStepSize"); + _throwIfNotInitialized('getExposureOffsetStepSize'); try { return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); } on PlatformException catch (e) { @@ -704,21 +707,21 @@ class CameraController extends ValueNotifier { /// /// Returns the (rounded) offset value that was set. Future setExposureOffset(double offset) async { - _throwIfNotInitialized("setExposureOffset"); + _throwIfNotInitialized('setExposureOffset'); // Check if offset is in range - List range = - await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); + final List range = await Future.wait( + >[getMinExposureOffset(), getMaxExposureOffset()]); if (offset < range[0] || offset > range[1]) { throw CameraException( - "exposureOffsetOutOfBounds", - "The provided exposure offset was outside the supported range for this device.", + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', ); } // Round to the closest step if needed - double stepSize = await getExposureOffsetStepSize(); + final double stepSize = await getExposureOffsetStepSize(); if (stepSize > 0) { - double inv = 1.0 / stepSize; + final double inv = 1.0 / stepSize; double roundedOffset = (offset * inv).roundToDouble() / inv; if (roundedOffset > range[1]) { roundedOffset = (offset * inv).floorToDouble() / inv; @@ -743,8 +746,8 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.lockCaptureOrientation( _cameraId, orientation ?? value.deviceOrientation); value = value.copyWith( - lockedCaptureOrientation: - Optional.fromNullable(orientation ?? value.deviceOrientation)); + lockedCaptureOrientation: Optional.of( + orientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -764,7 +767,8 @@ class CameraController extends ValueNotifier { Future unlockCaptureOrientation() async { try { await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); - value = value.copyWith(lockedCaptureOrientation: Optional.absent()); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -801,7 +805,7 @@ class CameraController extends ValueNotifier { if (_isDisposed) { return; } - unawaited(_deviceOrientationSubscription?.cancel()); + _unawaited(_deviceOrientationSubscription?.cancel()); _isDisposed = true; super.dispose(); if (_initCalled != null) { diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index 43fa763bed48..bfcad6626dd6 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -2,23 +2,37 @@ // 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/material.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +// TODO(stuartmorgan): Remove all of these classes in a breaking change, and +// vend the platform interface versions directly. See +// https://github.com/flutter/flutter/issues/104188 /// A single color plane of image data. /// /// The number and meaning of the planes in an image are determined by the /// format of the Image. class Plane { + Plane._fromPlatformInterface(CameraImagePlane plane) + : bytes = plane.bytes, + bytesPerPixel = plane.bytesPerPixel, + bytesPerRow = plane.bytesPerRow, + height = plane.height, + width = plane.width; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. Plane._fromPlatformData(Map data) - : bytes = data['bytes'], - bytesPerPixel = data['bytesPerPixel'], - bytesPerRow = data['bytesPerRow'], - height = data['height'], - width = data['width']; + : bytes = data['bytes'] as Uint8List, + bytesPerPixel = data['bytesPerPixel'] as int?, + bytesPerRow = data['bytesPerRow'] as int, + height = data['height'] as int?, + width = data['width'] as int?; /// Bytes representing this plane. final Uint8List bytes; @@ -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,16 +117,29 @@ 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'], - width = data['width'], - lensAperture = data['lensAperture'], - sensorExposureTime = data['sensorExposureTime'], - sensorSensitivity = data['sensorSensitivity'], - planes = List.unmodifiable(data['planes'] - .map((dynamic planeData) => Plane._fromPlatformData(planeData))); + height = data['height'] as int, + width = data['width'] as int, + lensAperture = data['lensAperture'] as double?, + sensorExposureTime = data['sensorExposureTime'] as int?, + sensorSensitivity = data['sensorSensitivity'] as double?, + planes = List.unmodifiable((data['planes'] as List) + .map((dynamic planeData) => + Plane._fromPlatformData(planeData as Map))); /// Format of the image provided. /// diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 5faa69f3cb9d..94ffca649fa6 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -10,7 +10,8 @@ import 'package:flutter/services.dart'; /// A widget showing a live camera preview. class CameraPreview extends StatelessWidget { /// Creates a preview widget for the given camera controller. - const CameraPreview(this.controller, {this.child}); + 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; @@ -21,16 +22,16 @@ class CameraPreview extends StatelessWidget { @override Widget build(BuildContext context) { return controller.value.isInitialized - ? ValueListenableBuilder( + ? ValueListenableBuilder( valueListenable: controller, - builder: (context, value, child) { + builder: (BuildContext context, Object? value, Widget? child) { return AspectRatio( aspectRatio: _isLandscape() ? controller.value.aspectRatio : (1 / controller.value.aspectRatio), child: Stack( fit: StackFit.expand, - children: [ + children: [ _wrapInRotatedBox(child: controller.buildPreview()), child ?? Container(), ], @@ -54,12 +55,14 @@ class CameraPreview extends StatelessWidget { } bool _isLandscape() { - return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] - .contains(_getApplicableOrientation()); + return [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight + ].contains(_getApplicableOrientation()); } int _getQuarterTurns() { - Map turns = { + final Map turns = { DeviceOrientation.portraitUp: 0, DeviceOrientation.landscapeRight: 1, DeviceOrientation.portraitDown: 2, diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index b8894d58ac3a..e9a07e6b806c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -1,40 +1,40 @@ name: camera -description: A Flutter plugin for getting information about and controlling the - camera on Android, iOS and Web. Supports previewing the camera feed, capturing images, capturing video, - and streaming image buffers to dart. -repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera +description: A Flutter plugin for controlling the camera. Supports previewing + the camera feed, capturing images and video, and streaming image buffers to + 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 +version: 0.10.0 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.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 - pedantic: ^1.10.0 - quiver: ^3.0.0 flutter_plugin_android_lifecycle: ^2.0.2 + quiver: ^3.0.0 dev_dependencies: - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter mockito: ^5.0.0 plugin_platform_interface: ^2.0.0 video_player: ^2.0.0 diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index 840770d1eed7..a9320e46dfb5 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -2,39 +2,42 @@ // 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', () { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); expect( - () => cameraController.startImageStream((image) => null), + () => cameraController.startImageStream((CameraImage image) => null), throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'startImageStream() was called on an uninitialized CameraController.', ), @@ -44,8 +47,8 @@ void main() { test('startImageStream() throws $CameraException when recording videos', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -57,9 +60,9 @@ void main() { cameraController.value.copyWith(isRecordingVideo: true); expect( - () => cameraController.startImageStream((image) => null), + () => cameraController.startImageStream((CameraImage image) => null), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'A video recording is already started.', 'startImageStream was called while a video is being recorded.', ))); @@ -67,8 +70,8 @@ void main() { test( 'startImageStream() throws $CameraException when already streaming images', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -78,41 +81,32 @@ void main() { cameraController.value = cameraController.value.copyWith(isStreamingImages: true); expect( - () => cameraController.startImageStream((image) => null), + () => cameraController.startImageStream((CameraImage image) => null), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'A camera has started streaming images.', 'startImageStream was called while a camera was streaming images.', ))); }); test('startImageStream() calls CameraPlatform', () async { - MethodChannelMock cameraChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera', - methods: {'startImageStream': {}}); - MethodChannelMock streamChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera/imageStream', - methods: {'listen': {}}); - - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); - await cameraController.startImageStream((image) => null); + 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', () { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -123,12 +117,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'stopImageStream() was called on an uninitialized CameraController.', ), @@ -138,21 +132,21 @@ void main() { test('stopImageStream() throws $CameraException when recording videos', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); - await cameraController.startImageStream((image) => null); + await cameraController.startImageStream((CameraImage image) => null); cameraController.value = cameraController.value.copyWith(isRecordingVideo: true); expect( cameraController.stopImageStream, throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'A video recording is already started.', 'stopImageStream was called while a video is being recorded.', ))); @@ -160,8 +154,8 @@ void main() { test('stopImageStream() throws $CameraException when not streaming images', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -171,38 +165,50 @@ void main() { expect( cameraController.stopImageStream, throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'No camera is streaming images', 'stopImageStream was called when no camera is streaming images.', ))); }); test('stopImageStream() intended behaviour', () async { - MethodChannelMock cameraChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera', - methods: {'startImageStream': {}, 'stopImageStream': {}}); - MethodChannelMock streamChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera/imageStream', - methods: {'listen': {}, 'cancel': {}}); - - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); - await cameraController.startImageStream((image) => null); + 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 85d613f41485..ecf4b509e2e4 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -2,28 +2,82 @@ // 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; - CameraImage cameraImage = CameraImage.fromPlatformData({ + final CameraImage cameraImage = + CameraImage.fromPlatformData({ 'format': 35, 'height': 1, 'width': 4, 'lensAperture': 1.8, 'sensorExposureTime': 9991324, 'sensorSensitivity': 92.0, - 'planes': [ - { - 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), 'bytesPerPixel': 1, 'bytesPerRow': 4, 'height': 1, @@ -40,16 +94,17 @@ void main() { test('$CameraImage has ImageFormatGroup.yuv420 for iOS', () { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - CameraImage cameraImage = CameraImage.fromPlatformData({ + final CameraImage cameraImage = + CameraImage.fromPlatformData({ 'format': 875704438, 'height': 1, 'width': 4, 'lensAperture': 1.8, 'sensorExposureTime': 9991324, 'sensorSensitivity': 92.0, - 'planes': [ - { - 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), 'bytesPerPixel': 1, 'bytesPerRow': 4, 'height': 1, @@ -63,16 +118,17 @@ void main() { test('$CameraImage has ImageFormatGroup.yuv420 for Android', () { debugDefaultTargetPlatformOverride = TargetPlatform.android; - CameraImage cameraImage = CameraImage.fromPlatformData({ + final CameraImage cameraImage = + CameraImage.fromPlatformData({ 'format': 35, 'height': 1, 'width': 4, 'lensAperture': 1.8, 'sensorExposureTime': 9991324, 'sensorSensitivity': 92.0, - 'planes': [ - { - 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), 'bytesPerPixel': 1, 'bytesPerRow': 4, 'height': 1, @@ -86,16 +142,17 @@ void main() { test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - CameraImage cameraImage = CameraImage.fromPlatformData({ + final CameraImage cameraImage = + CameraImage.fromPlatformData({ 'format': 1111970369, 'height': 1, 'width': 4, 'lensAperture': 1.8, 'sensorExposureTime': 9991324, 'sensorSensitivity': 92.0, - 'planes': [ - { - 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), 'bytesPerPixel': 1, 'bytesPerRow': 4, 'height': 1, @@ -106,16 +163,17 @@ void main() { expect(cameraImage.format.group, ImageFormatGroup.bgra8888); }); test('$CameraImage has ImageFormatGroup.unknown', () { - CameraImage cameraImage = CameraImage.fromPlatformData({ + final CameraImage cameraImage = + CameraImage.fromPlatformData({ 'format': null, 'height': 1, 'width': 4, 'lensAperture': 1.8, 'sensorExposureTime': 9991324, 'sensorSensitivity': 92.0, - 'planes': [ - { - 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), 'bytesPerPixel': 1, 'bytesPerRow': 4, 'height': 1, diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 32718f4d5169..fe2f4f4e35c7 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'; @@ -23,7 +21,7 @@ class FakeController extends ValueNotifier @override Widget buildPreview() { - return Texture(textureId: CameraController.kUninitializedCameraId); + return const Texture(textureId: CameraController.kUninitializedCameraId); } @override @@ -33,7 +31,7 @@ class FakeController extends ValueNotifier void debugCheckIsDisposed() {} @override - CameraDescription get description => CameraDescription( + CameraDescription get description => const CameraDescription( name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0); @override @@ -97,7 +95,7 @@ class FakeController extends ValueNotifier Future setZoomLevel(double zoom) async {} @override - Future startImageStream(onAvailable) async {} + Future startImageStream(onLatestImageAvailable onAvailable) async {} @override Future startVideoRecording() async {} @@ -136,10 +134,11 @@ void main() { isRecordingVideo: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: - Optional.fromNullable(DeviceOrientation.landscapeRight), - recordingOrientation: - Optional.fromNullable(DeviceOrientation.landscapeLeft), - previewSize: Size(480, 640), + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640), ); await tester.pumpWidget( @@ -150,7 +149,7 @@ void main() { ); expect(find.byType(RotatedBox), findsOneWidget); - RotatedBox rotatedBox = + final RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); expect(rotatedBox.quarterTurns, 3); @@ -169,10 +168,11 @@ void main() { isInitialized: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: - Optional.fromNullable(DeviceOrientation.landscapeRight), - recordingOrientation: - Optional.fromNullable(DeviceOrientation.landscapeLeft), - previewSize: Size(480, 640), + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640), ); await tester.pumpWidget( @@ -183,7 +183,7 @@ void main() { ); expect(find.byType(RotatedBox), findsOneWidget); - RotatedBox rotatedBox = + final RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); expect(rotatedBox.quarterTurns, 1); @@ -202,9 +202,9 @@ void main() { isInitialized: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: null, - recordingOrientation: - Optional.fromNullable(DeviceOrientation.landscapeLeft), - previewSize: Size(480, 640), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), + previewSize: const Size(480, 640), ); await tester.pumpWidget( @@ -215,7 +215,7 @@ void main() { ); expect(find.byType(RotatedBox), findsOneWidget); - RotatedBox rotatedBox = + final RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); expect(rotatedBox.quarterTurns, 0); @@ -229,7 +229,7 @@ void main() { final FakeController controller = FakeController(); controller.value = controller.value.copyWith( isInitialized: true, - previewSize: Size(480, 640), + previewSize: const Size(480, 640), ); await tester.pumpWidget( diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 6904e68ef89f..0d3195ba4b4b 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,21 +13,23 @@ 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'; -get mockAvailableCameras => [ - CameraDescription( +List get mockAvailableCameras => [ + const CameraDescription( name: 'camBack', lensDirection: CameraLensDirection.back, sensorOrientation: 90), - CameraDescription( + const CameraDescription( name: 'camFront', lensDirection: CameraLensDirection.front, sensorOrientation: 180), ]; -get mockInitializeCamera => 13; +int get mockInitializeCamera => 13; -get mockOnCameraInitializedEvent => CameraInitializedEvent( +CameraInitializedEvent get mockOnCameraInitializedEvent => + const CameraInitializedEvent( 13, 75, 75, @@ -38,16 +39,17 @@ get mockOnCameraInitializedEvent => CameraInitializedEvent( true, ); -get mockOnDeviceOrientationChangedEvent => - DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); +DeviceOrientationChangedEvent get mockOnDeviceOrientationChangedEvent => + const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); -get mockOnCameraClosingEvent => null; +CameraClosingEvent get mockOnCameraClosingEvent => const CameraClosingEvent(13); -get mockOnCameraErrorEvent => CameraErrorEvent(13, 'closing'); +CameraErrorEvent get mockOnCameraErrorEvent => + const CameraErrorEvent(13, 'closing'); XFile mockTakePicture = XFile('foo/bar.png'); -get mockVideoRecordingXFile => null; +XFile mockVideoRecordingXFile = XFile('foo/bar.mpeg'); bool mockPlatformException = false; @@ -57,7 +59,7 @@ void main() { group('camera', () { test('debugCheckIsDisposed should not throw assertion error when disposed', () { - final MockCameraDescription description = MockCameraDescription(); + const MockCameraDescription description = MockCameraDescription(); final CameraController controller = CameraController( description, ResolutionPreset.low, @@ -70,7 +72,7 @@ void main() { test('debugCheckIsDisposed should throw assertion error when not disposed', () { - final MockCameraDescription description = MockCameraDescription(); + const MockCameraDescription description = MockCameraDescription(); final CameraController controller = CameraController( description, ResolutionPreset.low, @@ -85,7 +87,7 @@ void main() { test('availableCameras() has camera', () async { CameraPlatform.instance = MockCameraPlatform(); - var camList = await availableCameras(); + final List camList = await availableCameras(); expect(camList, equals(mockAvailableCameras)); }); @@ -97,8 +99,8 @@ void main() { }); test('Can be initialized', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -106,13 +108,13 @@ void main() { await cameraController.initialize(); expect(cameraController.value.aspectRatio, 1); - expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.previewSize, const Size(75, 75)); expect(cameraController.value.isInitialized, isTrue); }); test('can be disposed', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -120,7 +122,7 @@ void main() { await cameraController.initialize(); expect(cameraController.value.aspectRatio, 1); - expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.previewSize, const Size(75, 75)); expect(cameraController.value.isInitialized, isTrue); await cameraController.dispose(); @@ -129,8 +131,8 @@ void main() { }); test('initialize() throws CameraException when disposed', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -138,7 +140,7 @@ void main() { await cameraController.initialize(); expect(cameraController.value.aspectRatio, 1); - expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.previewSize, const Size(75, 75)); expect(cameraController.value.isInitialized, isTrue); await cameraController.dispose(); @@ -148,7 +150,7 @@ void main() { expect( cameraController.initialize, throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'Error description', 'initialize was called on a disposed CameraController', ))); @@ -156,8 +158,8 @@ void main() { test('initialize() throws $CameraException on $PlatformException ', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -168,7 +170,7 @@ void main() { expect( cameraController.initialize, throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'foo', 'bar', ))); @@ -177,8 +179,8 @@ void main() { test('initialize() sets imageFormat', () async { debugDefaultTargetPlatformOverride = TargetPlatform.android; - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -192,8 +194,8 @@ void main() { }); test('prepareForVideoRecording() calls $CameraPlatform ', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -206,8 +208,8 @@ void main() { }); test('takePicture() throws $CameraException when uninitialized ', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -217,12 +219,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'takePicture() was called on an uninitialized CameraController.', ), @@ -232,8 +234,8 @@ void main() { test('takePicture() throws $CameraException when takePicture is true', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -245,29 +247,29 @@ void main() { expect( cameraController.takePicture(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'Previous capture has not returned yet.', 'takePicture was called before the previous capture returned.', ))); }); test('takePicture() returns $XFile', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); - XFile xFile = await cameraController.takePicture(); + final XFile xFile = await cameraController.takePicture(); expect(xFile.path, mockTakePicture.path); }); test('takePicture() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -278,7 +280,7 @@ void main() { expect( cameraController.takePicture(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'foo', 'bar', ))); @@ -287,8 +289,8 @@ void main() { test('startVideoRecording() throws $CameraException when uninitialized', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -299,12 +301,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'startVideoRecording() was called on an uninitialized CameraController.', ), @@ -313,8 +315,8 @@ void main() { }); test('startVideoRecording() throws $CameraException when recording videos', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -328,7 +330,7 @@ void main() { expect( cameraController.startVideoRecording(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'A video recording is already started.', 'startVideoRecording was called when a recording is already started.', ))); @@ -337,8 +339,8 @@ void main() { test( 'startVideoRecording() throws $CameraException when already streaming images', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -352,7 +354,7 @@ void main() { expect( cameraController.startVideoRecording(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'A camera has started streaming images.', 'startVideoRecording was called while a camera was streaming images.', ))); @@ -360,8 +362,8 @@ void main() { test('getMaxZoomLevel() throws $CameraException when uninitialized', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -372,12 +374,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'getMaxZoomLevel() was called on an uninitialized CameraController.', ), @@ -386,8 +388,8 @@ void main() { }); test('getMaxZoomLevel() throws $CameraException when disposed', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -401,12 +403,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Disposed CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'getMaxZoomLevel() was called on a disposed CameraController.', ), @@ -417,8 +419,8 @@ void main() { test( 'getMaxZoomLevel() throws $CameraException when a platform exception occured.', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -434,17 +436,18 @@ void main() { expect( cameraController.getMaxZoomLevel, throwsA(isA() - .having((error) => error.code, 'code', 'TEST_ERROR') .having( - (error) => error.description, + (CameraException error) => error.code, 'code', 'TEST_ERROR') + .having( + (CameraException error) => error.description, 'description', 'This is a test error messge', ))); }); test('getMaxZoomLevel() returns max zoom level.', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -452,16 +455,16 @@ void main() { await cameraController.initialize(); when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera)) - .thenAnswer((_) => Future.value(42.0)); + .thenAnswer((_) => Future.value(42.0)); - final maxZoomLevel = await cameraController.getMaxZoomLevel(); + final double maxZoomLevel = await cameraController.getMaxZoomLevel(); expect(maxZoomLevel, 42.0); }); test('getMinZoomLevel() throws $CameraException when uninitialized', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -472,12 +475,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'getMinZoomLevel() was called on an uninitialized CameraController.', ), @@ -486,8 +489,8 @@ void main() { }); test('getMinZoomLevel() throws $CameraException when disposed', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -501,12 +504,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Disposed CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'getMinZoomLevel() was called on a disposed CameraController.', ), @@ -517,8 +520,8 @@ void main() { test( 'getMinZoomLevel() throws $CameraException when a platform exception occured.', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -534,17 +537,18 @@ void main() { expect( cameraController.getMinZoomLevel, throwsA(isA() - .having((error) => error.code, 'code', 'TEST_ERROR') .having( - (error) => error.description, + (CameraException error) => error.code, 'code', 'TEST_ERROR') + .having( + (CameraException error) => error.description, 'description', 'This is a test error messge', ))); }); test('getMinZoomLevel() returns max zoom level.', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -552,15 +556,15 @@ void main() { await cameraController.initialize(); when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera)) - .thenAnswer((_) => Future.value(42.0)); + .thenAnswer((_) => Future.value(42.0)); - final maxZoomLevel = await cameraController.getMinZoomLevel(); + final double maxZoomLevel = await cameraController.getMinZoomLevel(); expect(maxZoomLevel, 42.0); }); test('setZoomLevel() throws $CameraException when uninitialized', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -571,12 +575,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Uninitialized CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'setZoomLevel() was called on an uninitialized CameraController.', ), @@ -585,8 +589,8 @@ void main() { }); test('setZoomLevel() throws $CameraException when disposed', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -600,12 +604,12 @@ void main() { throwsA( isA() .having( - (error) => error.code, + (CameraException error) => error.code, 'code', 'Disposed CameraController', ) .having( - (error) => error.description, + (CameraException error) => error.description, 'description', 'setZoomLevel() was called on a disposed CameraController.', ), @@ -616,8 +620,8 @@ void main() { test( 'setZoomLevel() throws $CameraException when a platform exception occured.', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -633,9 +637,10 @@ void main() { expect( () => cameraController.setZoomLevel(42), throwsA(isA() - .having((error) => error.code, 'code', 'TEST_ERROR') .having( - (error) => error.description, + (CameraException error) => error.code, 'code', 'TEST_ERROR') + .having( + (CameraException error) => error.description, 'description', 'This is a test error messge', ))); @@ -646,8 +651,8 @@ void main() { test( 'setZoomLevel() completes and calls method channel with correct value.', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -661,8 +666,8 @@ void main() { }); test('setFlashMode() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -678,8 +683,8 @@ void main() { test('setFlashMode() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -699,15 +704,15 @@ void main() { expect( cameraController.setFlashMode(FlashMode.always), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('setExposureMode() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -723,8 +728,8 @@ void main() { test('setExposureMode() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -744,32 +749,32 @@ void main() { expect( cameraController.setExposureMode(ExposureMode.auto), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('setExposurePoint() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); - await cameraController.setExposurePoint(Offset(0.5, 0.5)); + await cameraController.setExposurePoint(const Offset(0.5, 0.5)); verify(CameraPlatform.instance.setExposurePoint( - cameraController.cameraId, Point(0.5, 0.5))) + cameraController.cameraId, const Point(0.5, 0.5))) .called(1); }); test('setExposurePoint() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -777,7 +782,7 @@ void main() { await cameraController.initialize(); when(CameraPlatform.instance.setExposurePoint( - cameraController.cameraId, Point(0.5, 0.5))) + cameraController.cameraId, const Point(0.5, 0.5))) .thenThrow( PlatformException( code: 'TEST_ERROR', @@ -787,17 +792,17 @@ void main() { ); expect( - cameraController.setExposurePoint(Offset(0.5, 0.5)), + cameraController.setExposurePoint(const Offset(0.5, 0.5)), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('getMinExposureOffset() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -806,7 +811,7 @@ void main() { when(CameraPlatform.instance .getMinExposureOffset(cameraController.cameraId)) - .thenAnswer((_) => Future.value(0.0)); + .thenAnswer((_) => Future.value(0.0)); await cameraController.getMinExposureOffset(); @@ -817,8 +822,8 @@ void main() { test('getMinExposureOffset() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -837,15 +842,15 @@ void main() { expect( cameraController.getMinExposureOffset(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('getMaxExposureOffset() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -854,7 +859,7 @@ void main() { when(CameraPlatform.instance .getMaxExposureOffset(cameraController.cameraId)) - .thenAnswer((_) => Future.value(1.0)); + .thenAnswer((_) => Future.value(1.0)); await cameraController.getMaxExposureOffset(); @@ -865,8 +870,8 @@ void main() { test('getMaxExposureOffset() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -885,15 +890,15 @@ void main() { expect( cameraController.getMaxExposureOffset(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('getExposureOffsetStepSize() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -902,7 +907,7 @@ void main() { when(CameraPlatform.instance .getExposureOffsetStepSize(cameraController.cameraId)) - .thenAnswer((_) => Future.value(0.0)); + .thenAnswer((_) => Future.value(0.0)); await cameraController.getExposureOffsetStepSize(); @@ -914,8 +919,8 @@ void main() { test( 'getExposureOffsetStepSize() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -934,15 +939,15 @@ void main() { expect( cameraController.getExposureOffsetStepSize(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('setExposureOffset() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -970,8 +975,8 @@ void main() { test('setExposureOffset() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -998,7 +1003,7 @@ void main() { expect( cameraController.setExposureOffset(1.0), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); @@ -1007,8 +1012,8 @@ void main() { test( 'setExposureOffset() throws $CameraException when offset is out of bounds', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1036,14 +1041,14 @@ void main() { expect( cameraController.setExposureOffset(3.0), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'exposureOffsetOutOfBounds', 'The provided exposure offset was outside the supported range for this device.', ))); expect( cameraController.setExposureOffset(-2.0), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'exposureOffsetOutOfBounds', 'The provided exposure offset was outside the supported range for this device.', ))); @@ -1064,8 +1069,8 @@ void main() { }); test('setExposureOffset() rounds offset to nearest step', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1138,8 +1143,8 @@ void main() { }); test('pausePreview() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1159,8 +1164,8 @@ void main() { test('pausePreview() does not call $CameraPlatform when already paused', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1176,10 +1181,34 @@ 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 { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1197,15 +1226,15 @@ void main() { expect( cameraController.pausePreview(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('resumePreview() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1223,8 +1252,8 @@ void main() { test('resumePreview() does not call $CameraPlatform when not paused', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1242,8 +1271,8 @@ void main() { test('resumePreview() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1263,15 +1292,15 @@ void main() { expect( cameraController.resumePreview(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('lockCaptureOrientation() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1297,8 +1326,8 @@ void main() { test( 'lockCaptureOrientation() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1317,15 +1346,15 @@ void main() { expect( cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); }); test('unlockCaptureOrientation() calls $CameraPlatform', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1343,8 +1372,8 @@ void main() { test( 'unlockCaptureOrientation() throws $CameraException on $PlatformException', () async { - CameraController cameraController = CameraController( - CameraDescription( + final CameraController cameraController = CameraController( + const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), @@ -1363,7 +1392,7 @@ void main() { expect( cameraController.unlockCaptureOrientation(), throwsA(isA().having( - (error) => error.description, + (CameraException error) => error.description, 'TEST_ERROR', 'This is a test error message', ))); @@ -1381,20 +1410,20 @@ class MockCameraPlatform extends Mock }) async => super.noSuchMethod(Invocation.method( #initializeCamera, - [cameraId], - { + [cameraId], + { #imageFormatGroup: imageFormatGroup, }, )); @override Future dispose(int? cameraId) async { - return super.noSuchMethod(Invocation.method(#dispose, [cameraId])); + return super.noSuchMethod(Invocation.method(#dispose, [cameraId])); } @override Future> availableCameras() => - Future.value(mockAvailableCameras); + Future>.value(mockAvailableCameras); @override Future createCamera( @@ -1404,28 +1433,29 @@ class MockCameraPlatform extends Mock }) => mockPlatformException ? throw PlatformException(code: 'foo', message: 'bar') - : Future.value(mockInitializeCamera); + : Future.value(mockInitializeCamera); @override Stream onCameraInitialized(int cameraId) => - Stream.value(mockOnCameraInitializedEvent); + Stream.value(mockOnCameraInitializedEvent); @override Stream onCameraClosing(int cameraId) => - Stream.value(mockOnCameraClosingEvent); + Stream.value(mockOnCameraClosingEvent); @override Stream onCameraError(int cameraId) => - Stream.value(mockOnCameraErrorEvent); + Stream.value(mockOnCameraErrorEvent); @override Stream onDeviceOrientationChanged() => - Stream.value(mockOnDeviceOrientationChangedEvent); + Stream.value( + mockOnDeviceOrientationChangedEvent); @override Future takePicture(int cameraId) => mockPlatformException ? throw PlatformException(code: 'foo', message: 'bar') - : Future.value(mockTakePicture); + : Future.value(mockTakePicture); @override Future prepareForVideoRecording() async => @@ -1434,87 +1464,91 @@ class MockCameraPlatform extends Mock @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) => - Future.value(mockVideoRecordingXFile); + Future.value(mockVideoRecordingXFile); @override Future lockCaptureOrientation( int? cameraId, DeviceOrientation? orientation) async => - super.noSuchMethod( - Invocation.method(#lockCaptureOrientation, [cameraId, orientation])); + super.noSuchMethod(Invocation.method( + #lockCaptureOrientation, [cameraId, orientation])); @override - Future unlockCaptureOrientation(int? cameraId) async => super - .noSuchMethod(Invocation.method(#unlockCaptureOrientation, [cameraId])); + Future unlockCaptureOrientation(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#unlockCaptureOrientation, [cameraId])); @override Future pausePreview(int? cameraId) async => - super.noSuchMethod(Invocation.method(#pausePreview, [cameraId])); + super.noSuchMethod(Invocation.method(#pausePreview, [cameraId])); @override - Future resumePreview(int? cameraId) async => - super.noSuchMethod(Invocation.method(#resumePreview, [cameraId])); + Future resumePreview(int? cameraId) async => super + .noSuchMethod(Invocation.method(#resumePreview, [cameraId])); @override Future getMaxZoomLevel(int? cameraId) async => super.noSuchMethod( - Invocation.method(#getMaxZoomLevel, [cameraId]), - returnValue: 1.0, - ); + Invocation.method(#getMaxZoomLevel, [cameraId]), + returnValue: Future.value(1.0), + ) as Future; @override Future getMinZoomLevel(int? cameraId) async => super.noSuchMethod( - Invocation.method(#getMinZoomLevel, [cameraId]), - returnValue: 0.0, - ); + Invocation.method(#getMinZoomLevel, [cameraId]), + returnValue: Future.value(0.0), + ) as Future; @override Future setZoomLevel(int? cameraId, double? zoom) async => - super.noSuchMethod(Invocation.method(#setZoomLevel, [cameraId, zoom])); + super.noSuchMethod( + Invocation.method(#setZoomLevel, [cameraId, zoom])); @override Future setFlashMode(int? cameraId, FlashMode? mode) async => - super.noSuchMethod(Invocation.method(#setFlashMode, [cameraId, mode])); + super.noSuchMethod( + Invocation.method(#setFlashMode, [cameraId, mode])); @override Future setExposureMode(int? cameraId, ExposureMode? mode) async => - super.noSuchMethod(Invocation.method(#setExposureMode, [cameraId, mode])); + super.noSuchMethod( + Invocation.method(#setExposureMode, [cameraId, mode])); @override Future setExposurePoint(int? cameraId, Point? point) async => super.noSuchMethod( - Invocation.method(#setExposurePoint, [cameraId, point])); + Invocation.method(#setExposurePoint, [cameraId, point])); @override Future getMinExposureOffset(int? cameraId) async => super.noSuchMethod( - Invocation.method(#getMinExposureOffset, [cameraId]), - returnValue: 0.0, - ); + Invocation.method(#getMinExposureOffset, [cameraId]), + returnValue: Future.value(0.0), + ) as Future; @override Future getMaxExposureOffset(int? cameraId) async => super.noSuchMethod( - Invocation.method(#getMaxExposureOffset, [cameraId]), - returnValue: 1.0, - ); + Invocation.method(#getMaxExposureOffset, [cameraId]), + returnValue: Future.value(1.0), + ) as Future; @override Future getExposureOffsetStepSize(int? cameraId) async => super.noSuchMethod( - Invocation.method(#getExposureOffsetStepSize, [cameraId]), - returnValue: 1.0, - ); + Invocation.method(#getExposureOffsetStepSize, [cameraId]), + returnValue: Future.value(1.0), + ) as Future; @override Future setExposureOffset(int? cameraId, double? offset) async => super.noSuchMethod( - Invocation.method(#setExposureOffset, [cameraId, offset]), - returnValue: 1.0, - ); + Invocation.method(#setExposureOffset, [cameraId, offset]), + returnValue: Future.value(1.0), + ) as Future; } class MockCameraDescription extends CameraDescription { /// Creates a new camera description with the given properties. - MockCameraDescription() + const MockCameraDescription() : super( name: 'Test', lensDirection: CameraLensDirection.back, diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 4718d8943c34..e70f2ce69868 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'; @@ -13,7 +16,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('camera_value', () { test('Can be created', () { - var cameraValue = const CameraValue( + const CameraValue cameraValue = CameraValue( isInitialized: false, errorDescription: null, previewSize: Size(10, 10), @@ -36,7 +39,7 @@ void main() { expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); expect(cameraValue.errorDescription, null); - expect(cameraValue.previewSize, Size(10, 10)); + expect(cameraValue.previewSize, const Size(10, 10)); expect(cameraValue.isRecordingPaused, isFalse); expect(cameraValue.isRecordingVideo, isFalse); expect(cameraValue.isTakingPicture, isFalse); @@ -53,7 +56,7 @@ void main() { }); test('Can be created as uninitialized', () { - var cameraValue = const CameraValue.uninitialized(); + const CameraValue cameraValue = CameraValue.uninitialized(); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); @@ -75,8 +78,8 @@ void main() { }); test('Can be copied with isInitialized', () { - var cv = const CameraValue.uninitialized(); - var cameraValue = cv.copyWith(isInitialized: true); + const CameraValue cv = CameraValue.uninitialized(); + final CameraValue cameraValue = cv.copyWith(isInitialized: true); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isTrue); @@ -98,24 +101,24 @@ void main() { }); test('Has aspectRatio after setting size', () { - var cv = const CameraValue.uninitialized(); - var cameraValue = - cv.copyWith(isInitialized: true, previewSize: Size(20, 10)); + const CameraValue cv = CameraValue.uninitialized(); + final CameraValue cameraValue = + cv.copyWith(isInitialized: true, previewSize: const Size(20, 10)); expect(cameraValue.aspectRatio, 2.0); }); test('hasError is true after setting errorDescription', () { - var cv = const CameraValue.uninitialized(); - var cameraValue = cv.copyWith(errorDescription: 'error'); + const CameraValue cv = CameraValue.uninitialized(); + final CameraValue cameraValue = cv.copyWith(errorDescription: 'error'); expect(cameraValue.hasError, isTrue); expect(cameraValue.errorDescription, 'error'); }); test('Recording paused is false when not recording', () { - var cv = const CameraValue.uninitialized(); - var cameraValue = cv.copyWith( + const CameraValue cv = CameraValue.uninitialized(); + final CameraValue cameraValue = cv.copyWith( isInitialized: true, isRecordingVideo: false, isRecordingPaused: true); @@ -124,7 +127,7 @@ void main() { }); test('toString() works as expected', () { - var cameraValue = const CameraValue( + const CameraValue cameraValue = CameraValue( isInitialized: false, errorDescription: null, previewSize: Size(10, 10), diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera/test/utils/method_channel_mock.dart deleted file mode 100644 index 60d8def6a2e3..000000000000 --- a/packages/camera/camera/test/utils/method_channel_mock.dart +++ /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 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class MethodChannelMock { - final Duration? delay; - final MethodChannel methodChannel; - final Map methods; - final log = []; - - MethodChannelMock({ - required String channelName, - this.delay, - required this.methods, - }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); - } - - Future _handler(MethodCall methodCall) async { - log.add(methodCall); - - if (!methods.containsKey(methodCall.method)) { - throw MissingPluginException('No implementation found for method ' - '${methodCall.method} on channel ${methodChannel.name}'); - } - - return Future.delayed(delay ?? Duration.zero, () { - final result = methods[methodCall.method]; - if (result is Exception) { - throw result; - } - - return Future.value(result); - }); - } -} diff --git a/packages/android_alarm_manager/AUTHORS b/packages/camera/camera_android/AUTHORS similarity index 100% rename from packages/android_alarm_manager/AUTHORS rename to packages/camera/camera_android/AUTHORS diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md new file mode 100644 index 000000000000..3a743340dc6e --- /dev/null +++ b/packages/camera/camera_android/CHANGELOG.md @@ -0,0 +1,24 @@ +## 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/android_alarm_manager/LICENSE b/packages/camera/camera_android/LICENSE similarity index 100% rename from packages/android_alarm_manager/LICENSE rename to packages/camera/camera_android/LICENSE 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/android/build.gradle b/packages/camera/camera_android/android/build.gradle new file mode 100644 index 000000000000..202767632117 --- /dev/null +++ b/packages/camera/camera_android/android/build.gradle @@ -0,0 +1,67 @@ +group 'io.flutter.plugins.camera' +version '1.0-SNAPSHOT' +def args = ["-Xlint:deprecation","-Xlint:unchecked"] + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.2' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +project.getTasks().withType(JavaCompile){ + options.compilerArgs.addAll(args) +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + targetSdkVersion 31 + minSdkVersion 21 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + compileOnly 'androidx.annotation:annotation:1.1.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:4.0.0' + testImplementation 'androidx.test:core:1.3.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 87% 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 4601e7d34d69..401963c91374 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 @@ -20,6 +20,7 @@ import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; +import android.media.EncoderProfiles; import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; @@ -35,9 +36,7 @@ import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; +import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel; @@ -80,10 +79,27 @@ 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, - LifecycleObserver { + ImageReader.OnImageAvailableListener { private static final String TAG = "Camera"; private static final HashMap supportedImageFormats; @@ -115,8 +131,10 @@ class Camera /** An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread backgroundHandlerThread; + /** True when backgroundHandlerThread is in the process of being stopped. */ + private boolean stoppingBackgroundHandlerThread = false; - private CameraDevice cameraDevice; + private CameraDeviceWrapper cameraDevice; private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; @@ -138,6 +156,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, @@ -202,8 +258,16 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) .getLockedCaptureOrientation(); + MediaRecorderBuilder mediaRecorderBuilder; + + if (Build.VERSION.SDK_INT >= 31) { + mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath); + } else { + mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath); + } + mediaRecorder = - new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + mediaRecorderBuilder .setEnableAudio(enableAudio) .setMediaOrientation( lockedOrientation == null @@ -255,7 +319,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( @@ -275,8 +339,10 @@ public void onOpened(@NonNull CameraDevice device) { public void onClosed(@NonNull CameraDevice camera) { Log.i(TAG, "open | onClosed"); + // Prevents calls to methods that would otherwise result in IllegalStateException exceptions. + cameraDevice = null; + closeCaptureSession(); dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); } @Override @@ -318,8 +384,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); } @@ -327,7 +393,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); @@ -358,10 +424,13 @@ private void createCaptureSession( // Prepare the callback. CameraCaptureSession.StateCallback callback = new CameraCaptureSession.StateCallback() { + boolean captureSessionClosed = false; + @Override public void onConfigured(@NonNull CameraCaptureSession session) { + Log.i(TAG, "CameraCaptureSession onConfigured"); // Camera was already closed. - if (cameraDevice == null) { + if (cameraDevice == null || captureSessionClosed) { dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); return; } @@ -376,8 +445,15 @@ public void onConfigured(@NonNull CameraCaptureSession session) { @Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + Log.i(TAG, "CameraCaptureSession onConfigureFailed"); dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); } + + @Override + public void onClosed(@NonNull CameraCaptureSession session) { + Log.i(TAG, "CameraCaptureSession onClosed"); + captureSessionClosed = true; + } }; // Start the session. @@ -421,10 +497,12 @@ private void createCaptureSession( // Send a repeating request to refresh capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + Log.i(TAG, "refreshPreviewCaptureSession"); + if (captureSession == null) { Log.i( TAG, - "[refreshPreviewCaptureSession] captureSession not yet initialized, " + "refreshPreviewCaptureSession: captureSession not yet initialized, " + "skipping preview capture session refresh."); return; } @@ -439,6 +517,8 @@ private void refreshPreviewCaptureSession( onSuccessCallback.run(); } + } catch (IllegalStateException e) { + onErrorCallback.onError("cameraAccess", "Camera is closed: " + e.getMessage()); } catch (CameraAccessException e) { onErrorCallback.onError("cameraAccess", e.getMessage()); } @@ -562,7 +642,6 @@ public void onCaptureCompleted( try { captureSession.stopRepeating(); - captureSession.abortCaptures(); Log.i(TAG, "sending capture request"); captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); } catch (CameraAccessException e) { @@ -576,21 +655,27 @@ private Display getDefaultDisplay() { } /** Starts a background thread and its {@link Handler}. */ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void startBackgroundThread() { - backgroundHandlerThread = new HandlerThread("CameraBackground"); + if (backgroundHandlerThread != null) { + return; + } + + backgroundHandlerThread = HandlerThreadFactory.create("CameraBackground"); try { backgroundHandlerThread.start(); } catch (IllegalThreadStateException e) { // Ignore exception in case the thread has already started. } - backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); + backgroundHandler = HandlerFactory.create(backgroundHandlerThread.getLooper()); } /** Stops the background thread and its {@link Handler}. */ - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void stopBackgroundThread() { + if (stoppingBackgroundHandlerThread) { + return; + } if (backgroundHandlerThread != null) { + stoppingBackgroundHandlerThread = true; backgroundHandlerThread.quitSafely(); try { backgroundHandlerThread.join(); @@ -600,6 +685,7 @@ public void stopBackgroundThread() { } backgroundHandlerThread = null; backgroundHandler = null; + stoppingBackgroundHandlerThread = false; } /** Start capturing a picture, doing autofocus first. */ @@ -919,8 +1005,12 @@ public float getMinZoomLevel() { return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); } - /** Shortcut to get current recording profile. */ - CamcorderProfile getRecordingProfile() { + /** Shortcut to get current recording profile. Legacy method provides support for SDK < 31. */ + CamcorderProfile getRecordingProfileLegacy() { + return cameraFeatures.getResolution().getRecordingProfileLegacy(); + } + + EncoderProfiles getRecordingProfile() { return cameraFeatures.getResolution().getRecordingProfile(); } @@ -1090,12 +1180,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; @@ -1120,4 +1217,38 @@ public void dispose() { flutterTexture.release(); getDeviceOrientationManager().stop(); } + + /** Factory class that assists in creating a {@link HandlerThread} instance. */ + static class HandlerThreadFactory { + /** + * Creates a new instance of the {@link HandlerThread} class. + * + *

This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param name to give to the HandlerThread. + * @return new instance of the {@link HandlerThread} class. + */ + @VisibleForTesting + public static HandlerThread create(String name) { + return new HandlerThread(name); + } + } + + /** Factory class that assists in creating a {@link Handler} instance. */ + static class HandlerFactory { + /** + * Creates a new instance of the {@link Handler} class. + * + *

This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param looper to give to the Handler. + * @return new instance of the {@link Handler} class. + */ + @VisibleForTesting + public static Handler create(Looper looper) { + return new Handler(looper); + } + } } 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/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 new file mode 100644 index 000000000000..4441751e19cf --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -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. + +package io.flutter.plugins.camera; + +import android.Manifest; +import android.Manifest.permission; +import android.app.Activity; +import android.content.pm.PackageManager; +import androidx.annotation.VisibleForTesting; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +final class CameraPermissions { + interface PermissionsRegistry { + @SuppressWarnings("deprecation") + void addListener( + io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener handler); + } + + interface ResultCallback { + void onResult(String errorCode, String errorDescription); + } + + /** + * Camera access permission errors handled when camera is created. See {@code MethodChannelCamera} + * in {@code camera/camera_platform_interface} for details. + */ + private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING = + "CameraPermissionsRequestOngoing"; + + private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = + "Another request is ongoing and multiple requests cannot be handled at once."; + private static final String CAMERA_ACCESS_DENIED = "CameraAccessDenied"; + private static final String CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."; + private static final String AUDIO_ACCESS_DENIED = "AudioAccessDenied"; + private static final String AUDIO_ACCESS_DENIED_MESSAGE = "Audio access permission was denied."; + + private static final int CAMERA_REQUEST_ID = 9796; + @VisibleForTesting boolean ongoing = false; + + void requestPermissions( + Activity activity, + PermissionsRegistry permissionsRegistry, + boolean enableAudio, + ResultCallback callback) { + if (ongoing) { + callback.onResult( + CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE); + return; + } + if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { + permissionsRegistry.addListener( + new CameraRequestPermissionsListener( + (String errorCode, String errorDescription) -> { + ongoing = false; + callback.onResult(errorCode, errorDescription); + })); + ongoing = true; + ActivityCompat.requestPermissions( + activity, + enableAudio + ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO} + : new String[] {Manifest.permission.CAMERA}, + CAMERA_REQUEST_ID); + } else { + // Permissions already exist. Call the callback with success. + callback.onResult(null, null); + } + } + + private boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.CAMERA) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean hasAudioPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + } + + @VisibleForTesting + @SuppressWarnings("deprecation") + static final class CameraRequestPermissionsListener + implements io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener { + + // There's no way to unregister permission listeners in the v1 embedding, so we'll be called + // duplicate times in cases where the user denies and then grants a permission. Keep track of if + // we've responded before and bail out of handling the callback manually if this is a repeat + // call. + boolean alreadyCalled = false; + + final ResultCallback callback; + + @VisibleForTesting + CameraRequestPermissionsListener(ResultCallback callback) { + this.callback = callback; + } + + @Override + public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { + if (alreadyCalled || id != CAMERA_REQUEST_ID) { + return false; + } + + alreadyCalled = true; + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + callback.onResult(CAMERA_ACCESS_DENIED, CAMERA_ACCESS_DENIED_MESSAGE); + } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) { + callback.onResult(AUDIO_ACCESS_DENIED, AUDIO_ACCESS_DENIED_MESSAGE); + } else { + callback.onResult(null, null); + } + return true; + } + } +} diff --git a/packages/camera/camera/android/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 87% 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 index ef3a2b9b5d83..067ed0295e2e 100644 --- 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 @@ -8,11 +8,9 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.Lifecycle; 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.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.view.TextureRegistry; @@ -53,8 +51,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.activity(), registrar.messenger(), registrar::addRequestPermissionsResultListener, - registrar.view(), - null); + registrar.view()); } @Override @@ -73,8 +70,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { binding.getActivity(), flutterPluginBinding.getBinaryMessenger(), binding::addRequestPermissionsResultListener, - flutterPluginBinding.getTextureRegistry(), - FlutterLifecycleAdapter.getActivityLifecycle(binding)); + flutterPluginBinding.getTextureRegistry()); } @Override @@ -100,8 +96,7 @@ private void maybeStartListening( Activity activity, BinaryMessenger messenger, PermissionsRegistry permissionsRegistry, - TextureRegistry textureRegistry, - @Nullable Lifecycle lifecycle) { + TextureRegistry textureRegistry) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin. return; @@ -109,11 +104,6 @@ private void maybeStartListening( methodCallHandler = new MethodCallHandlerImpl( - activity, - messenger, - new CameraPermissions(), - permissionsRegistry, - textureRegistry, - lifecycle); + activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry); } } 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 95% 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 index 003d80a6c241..11b6eeaa5b50 100644 --- 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 @@ -97,6 +97,16 @@ public static List> getAvailableCameras(Activity activity) String[] cameraNames = cameraManager.getCameraIdList(); List> cameras = new ArrayList<>(); for (String cameraName : cameraNames) { + int cameraId; + try { + cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + cameraId = -1; + } + if (cameraId < 0) { + continue; + } + HashMap details = new HashMap<>(); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); details.put("name", cameraName); 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/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 new file mode 100644 index 000000000000..38201e1136c9 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -0,0 +1,414 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; +import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import java.util.Map; + +final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + private final Activity activity; + private final BinaryMessenger messenger; + private final CameraPermissions cameraPermissions; + private final PermissionsRegistry permissionsRegistry; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + private final EventChannel imageStreamChannel; + private @Nullable Camera camera; + + MethodCallHandlerImpl( + Activity activity, + BinaryMessenger messenger, + CameraPermissions cameraPermissions, + PermissionsRegistry permissionsAdder, + TextureRegistry textureRegistry) { + this.activity = activity; + this.messenger = messenger; + this.cameraPermissions = cameraPermissions; + this.permissionsRegistry = permissionsAdder; + this.textureRegistry = textureRegistry; + + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera_android"); + imageStreamChannel = + new EventChannel(messenger, "plugins.flutter.io/camera_android/imageStream"); + methodChannel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "availableCameras": + try { + result.success(CameraUtils.getAvailableCameras(activity)); + } catch (Exception e) { + handleException(e, result); + } + break; + case "create": + { + if (camera != null) { + camera.close(); + } + + cameraPermissions.requestPermissions( + activity, + permissionsRegistry, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); + } + }); + break; + } + case "initialize": + { + if (camera != null) { + try { + camera.open(call.argument("imageFormatGroup")); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error( + "cameraNotFound", + "Camera not found. Please call the 'create' method before calling 'initialize'.", + null); + } + break; + } + case "takePicture": + { + camera.takePicture(result); + break; + } + case "prepareForVideoRecording": + { + // This optimization is not required for Android. + result.success(null); + break; + } + case "startVideoRecording": + { + camera.startVideoRecording(result); + break; + } + case "stopVideoRecording": + { + camera.stopVideoRecording(result); + break; + } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } + case "setFlashMode": + { + String modeStr = call.argument("mode"); + FlashMode mode = FlashMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); + return; + } + try { + camera.setFlashMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureMode": + { + String modeStr = call.argument("mode"); + ExposureMode mode = ExposureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); + return; + } + try { + camera.setExposureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposurePoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setExposurePoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": + { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": + { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": + { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": + { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusMode": + { + String modeStr = call.argument("mode"); + FocusMode mode = FocusMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); + return; + } + try { + camera.setFocusMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusPoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setFocusPoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "startImageStream": + { + try { + camera.startPreviewWithImageStream(imageStreamChannel); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "stopImageStream": + { + try { + camera.startPreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxZoomLevel": + { + assert camera != null; + + try { + float maxZoomLevel = camera.getMaxZoomLevel(); + result.success(maxZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinZoomLevel": + { + assert camera != null; + + try { + float minZoomLevel = camera.getMinZoomLevel(); + result.success(minZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setZoomLevel": + { + assert camera != null; + + Double zoom = call.argument("zoom"); + + if (zoom == null) { + result.error( + "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); + return; + } + + try { + camera.setZoomLevel(result, zoom.floatValue()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "lockCaptureOrientation": + { + PlatformChannel.DeviceOrientation orientation = + CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": + { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "pausePreview": + { + try { + camera.pausePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "resumePreview": + { + camera.resumePreview(); + result.success(null); + break; + } + case "dispose": + { + if (camera != null) { + camera.dispose(); + } + result.success(null); + break; + } + default: + result.notImplemented(); + break; + } + } + + void stopListening() { + methodChannel.setMethodCallHandler(null); + } + + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String preset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); + + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = + textureRegistry.createSurfaceTexture(); + DartMessenger dartMessenger = + new DartMessenger( + messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + + camera = + new Camera( + activity, + flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), + dartMessenger, + cameraProperties, + resolutionPreset, + enableAudio); + + Map reply = new HashMap<>(); + reply.put("cameraId", flutterSurfaceTexture.id()); + result.success(reply); + } + + // We move catching CameraAccessException out of onMethodCall because it causes a crash + // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to + // to be able to compile with <21 sdks for apps that want the camera and support earlier version. + @SuppressWarnings("ConstantConditions") + private void handleException(Exception exception, Result result) { + if (exception instanceof CameraAccessException) { + result.error("CameraAccess", exception.getMessage(), null); + return; + } + + // CameraAccessException can not be cast to a RuntimeException. + throw (RuntimeException) exception; + } +} 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/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 new file mode 100644 index 000000000000..afbd7c3758a6 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -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. + +package io.flutter.plugins.camera.features.resolution; + +import android.annotation.TargetApi; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.os.Build; +import android.util.Size; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import java.util.List; + +/** + * Controls the resolutions configuration on the {@link android.hardware.camera2} API. + * + *

The {@link ResolutionFeature} is responsible for converting the platform independent {@link + * ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties + * required to configure the resolution using the {@link android.hardware.camera2} API. + */ +public class ResolutionFeature extends CameraFeature { + private Size captureSize; + private Size previewSize; + private CamcorderProfile recordingProfileLegacy; + private EncoderProfiles recordingProfile; + private ResolutionPreset currentSetting; + private int cameraId; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param resolutionPreset Platform agnostic enum containing resolution information. + * @param cameraName Camera identifier of the camera for which to configure the resolution. + */ + public ResolutionFeature( + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { + super(cameraProperties); + this.currentSetting = resolutionPreset; + try { + this.cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + this.cameraId = -1; + return; + } + configureResolution(resolutionPreset, cameraId); + } + + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ + public CamcorderProfile getRecordingProfileLegacy() { + return this.recordingProfileLegacy; + } + + public EncoderProfiles getRecordingProfile() { + return this.recordingProfile; + } + + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ + public Size getPreviewSize() { + return this.previewSize; + } + + /** + * Gets the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ + public Size getCaptureSize() { + return this.captureSize; + } + + @Override + public String getDebugName() { + return "ResolutionFeature"; + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + configureResolution(currentSetting, cameraId); + } + + @Override + public boolean checkIsSupported() { + return cameraId >= 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) + throws IndexOutOfBoundsException { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + if (Build.VERSION.SDK_INT >= 31) { + EncoderProfiles profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + List videoProfiles = profile.getVideoProfiles(); + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); + + return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } else { + @SuppressWarnings("deprecation") + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + } + + /** + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link + * ResolutionPreset}. Supports SDK < 31. + * + * @param cameraId Camera identifier which indicates the device's camera for which to select a + * {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link + * android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied + * {@link ResolutionPreset}. + */ + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } + + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + } + + @TargetApi(Build.VERSION_CODES.S) + public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } + + String cameraIdString = Integer.toString(cameraId); + + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_LOW); + } + + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) + throws IndexOutOfBoundsException { + if (!checkIsSupported()) { + return; + } + + if (Build.VERSION.SDK_INT >= 31) { + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + List videoProfiles = recordingProfile.getVideoProfiles(); + + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); + captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } else { + @SuppressWarnings("deprecation") + CamcorderProfile camcorderProfile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset); + recordingProfileLegacy = camcorderProfile; + captureSize = + new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight); + } + + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); + } +} 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/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 new file mode 100644 index 000000000000..0aebfee39e0a --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.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.camera.media; + +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.media.MediaRecorder; +import android.os.Build; +import androidx.annotation.NonNull; +import java.io.IOException; + +public class MediaRecorderBuilder { + @SuppressWarnings("deprecation") + static class MediaRecorderFactory { + MediaRecorder makeMediaRecorder() { + return new MediaRecorder(); + } + } + + private final String outputFilePath; + private final CamcorderProfile camcorderProfile; + private final EncoderProfiles encoderProfiles; + private final MediaRecorderFactory recorderFactory; + + private boolean enableAudio; + private int mediaOrientation; + + public MediaRecorderBuilder( + @NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath) { + this(camcorderProfile, outputFilePath, new MediaRecorderFactory()); + } + + public MediaRecorderBuilder( + @NonNull EncoderProfiles encoderProfiles, @NonNull String outputFilePath) { + this(encoderProfiles, outputFilePath, new MediaRecorderFactory()); + } + + MediaRecorderBuilder( + @NonNull CamcorderProfile camcorderProfile, + @NonNull String outputFilePath, + MediaRecorderFactory helper) { + this.outputFilePath = outputFilePath; + this.camcorderProfile = camcorderProfile; + this.encoderProfiles = null; + this.recorderFactory = helper; + } + + MediaRecorderBuilder( + @NonNull EncoderProfiles encoderProfiles, + @NonNull String outputFilePath, + MediaRecorderFactory helper) { + this.outputFilePath = outputFilePath; + this.encoderProfiles = encoderProfiles; + this.camcorderProfile = null; + this.recorderFactory = helper; + } + + public MediaRecorderBuilder setEnableAudio(boolean enableAudio) { + this.enableAudio = enableAudio; + return this; + } + + public MediaRecorderBuilder setMediaOrientation(int orientation) { + this.mediaOrientation = orientation; + return this; + } + + public MediaRecorder build() throws IOException, NullPointerException, IndexOutOfBoundsException { + MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); + + // There's a fixed order that mediaRecorder expects. Only change these functions accordingly. + // You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder. + if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + + if (Build.VERSION.SDK_INT >= 31) { + EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0); + EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0); + + mediaRecorder.setOutputFormat(encoderProfiles.getRecommendedFileFormat()); + if (enableAudio) { + mediaRecorder.setAudioEncoder(audioProfile.getCodec()); + mediaRecorder.setAudioEncodingBitRate(audioProfile.getBitrate()); + mediaRecorder.setAudioSamplingRate(audioProfile.getSampleRate()); + } + mediaRecorder.setVideoEncoder(videoProfile.getCodec()); + mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate()); + mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate()); + mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + } else { + mediaRecorder.setOutputFormat(camcorderProfile.fileFormat); + if (enableAudio) { + mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec); + mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate); + mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate); + } + mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec); + mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate); + mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate); + mediaRecorder.setVideoSize( + camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight); + } + + mediaRecorder.setOutputFile(outputFilePath); + mediaRecorder.setOrientationHint(this.mediaOrientation); + + mediaRecorder.prepare(); + + return mediaRecorder; + } +} 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 84% 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 fbed28bc11fc..b85b685ca90b 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 @@ -5,28 +5,40 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; +import android.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.media.CamcorderProfile; +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; @@ -46,9 +58,38 @@ 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; @@ -57,6 +98,10 @@ public class CameraTest { private Camera camera; private CameraCaptureSession mockCaptureSession; private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; @Before public void before() { @@ -65,6 +110,10 @@ public void before() { mockDartMessenger = mock(DartMessenger.class); mockCaptureSession = mock(CameraCaptureSession.class); mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class); + mockHandlerThreadFactory = mockStatic(Camera.HandlerThreadFactory.class); + mockHandlerThread = mock(HandlerThread.class); + mockHandlerFactory = mockStatic(Camera.HandlerFactory.class); + mockHandler = mock(Handler.class); final Activity mockActivity = mock(Activity.class); final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = @@ -74,6 +123,10 @@ public void before() { final boolean enableAudio = false; when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + mockHandlerFactory.when(() -> Camera.HandlerFactory.create(any())).thenReturn(mockHandler); + mockHandlerThreadFactory + .when(() -> Camera.HandlerThreadFactory.create(any())) + .thenReturn(mockHandlerThread); camera = new Camera( @@ -92,6 +145,15 @@ public void before() { @After public void after() { TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0); + mockHandlerThreadFactory.close(); + mockHandlerFactory.close(); + } + + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class cameraClass = Camera.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass)); } @Test @@ -222,20 +284,6 @@ public void getMinZoomLevel() { assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); } - @Test - public void getRecordingProfile() { - ResolutionFeature mockResolutionFeature = - mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); - CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); - - when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); - - CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); - - verify(mockResolutionFeature, times(1)).getRecordingProfile(); - assertEquals(mockCamcorderProfile, actualRecordingProfile); - } - @Test public void setExposureMode_shouldUpdateExposureLockFeature() { ExposureLockFeature mockExposureLockFeature = @@ -773,6 +821,113 @@ public void resumePreview_shouldSendErrorEventOnCameraAccessException() verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); } + @Test + public void startBackgroundThread_shouldStartNewThread() { + camera.startBackgroundThread(); + + verify(mockHandlerThread, times(1)).start(); + assertEquals(mockHandler, TestUtils.getPrivateField(camera, "backgroundHandler")); + } + + @Test + public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() { + camera.startBackgroundThread(); + camera.startBackgroundThread(); + + verify(mockHandlerThread, times(1)).start(); + } + + @Test + public void stopBackgroundThread_cancelsDuplicateCalls() throws InterruptedException { + TestUtils.setPrivateField(camera, "stoppingBackgroundHandlerThread", true); + + camera.startBackgroundThread(); + camera.stopBackgroundThread(); + + verify(mockHandlerThread, never()).quitSafely(); + verify(mockHandlerThread, never()).join(); + } + + @Test + public void stopBackgroundThread_proceedsWithoutDuplicateCall() throws InterruptedException { + TestUtils.setPrivateField(camera, "stoppingBackgroundHandlerThread", false); + + camera.startBackgroundThread(); + camera.stopBackgroundThread(); + + verify(mockHandlerThread).quitSafely(); + verify(mockHandlerThread).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/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 new file mode 100644 index 000000000000..04bab14f26ac --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java @@ -0,0 +1,205 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class CameraTest_getRecordingProfileTest { + + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + private CameraCaptureSession mockCaptureSession; + private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + } + + @Config(maxSdk = 30) + @Test + public void getRecordingProfileLegacy() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfileLegacy()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfileLegacy(); + + verify(mockResolutionFeature, times(1)).getRecordingProfileLegacy(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Config(minSdk = 31) + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + EncoderProfiles mockRecordingProfile = mock(EncoderProfiles.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockRecordingProfile); + + EncoderProfiles actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockRecordingProfile, actualRecordingProfile); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrienttionFeature) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} diff --git a/packages/camera/camera_android/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 new file mode 100644 index 000000000000..e59b05bf4fe3 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -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. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class CameraUtilsTest { + + @Test + public void serializeDeviceOrientation_serializesCorrectly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + + @Test(expected = UnsupportedOperationException.class) + public void serializeDeviceOrientation_throws_for_null() { + CameraUtils.serializeDeviceOrientation(null); + } + + @Test + public void deserializeDeviceOrientation_deserializesCorrectly() { + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); + } + + @Test(expected = UnsupportedOperationException.class) + public void deserializeDeviceOrientation_throwsForNull() { + CameraUtils.deserializeDeviceOrientation(null); + } + + @Test + public void getAvailableCameras_retrievesValidCameras() + throws CameraAccessException, NumberFormatException { + final Activity mockActivity = mock(Activity.class); + final CameraManager mockCameraManager = mock(CameraManager.class); + final CameraCharacteristics mockCameraCharacteristics = mock(CameraCharacteristics.class); + final String[] mockCameraIds = {"1394902", "-192930", "0283835", "foobar"}; + final int mockSensorOrientation0 = 90; + final int mockSensorOrientation2 = 270; + final int mockLensFacing0 = CameraMetadata.LENS_FACING_FRONT; + final int mockLensFacing2 = CameraMetadata.LENS_FACING_EXTERNAL; + + when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager); + when(mockCameraManager.getCameraIdList()).thenReturn(mockCameraIds); + when(mockCameraManager.getCameraCharacteristics(anyString())) + .thenReturn(mockCameraCharacteristics); + when(mockCameraCharacteristics.get(any())) + .thenReturn(mockSensorOrientation0) + .thenReturn(mockLensFacing0) + .thenReturn(mockSensorOrientation2) + .thenReturn(mockLensFacing2); + + List> availableCameras = CameraUtils.getAvailableCameras(mockActivity); + + assertEquals(availableCameras.size(), 2); + assertEquals(availableCameras.get(0).get("name"), "1394902"); + assertEquals(availableCameras.get(0).get("sensorOrientation"), mockSensorOrientation0); + assertEquals(availableCameras.get(0).get("lensFacing"), "front"); + assertEquals(availableCameras.get(1).get("name"), "0283835"); + assertEquals(availableCameras.get(1).get("sensorOrientation"), mockSensorOrientation2); + assertEquals(availableCameras.get(1).get("lensFacing"), "external"); + } +} 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/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 new file mode 100644 index 000000000000..868e2e9e6d57 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java @@ -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. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import androidx.lifecycle.LifecycleObserver; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; + +public class MethodCallHandlerImplTest { + + MethodChannel.MethodCallHandler handler; + MethodChannel.Result mockResult; + Camera mockCamera; + + @Before + public void setUp() { + handler = + new MethodCallHandlerImpl( + mock(Activity.class), + mock(BinaryMessenger.class), + mock(CameraPermissions.class), + mock(CameraPermissions.PermissionsRegistry.class), + mock(TextureRegistry.class)); + mockResult = mock(MethodChannel.Result.class); + mockCamera = mock(Camera.class); + TestUtils.setPrivateField(handler, "camera", mockCamera); + } + + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class methodCallHandlerClass = MethodCallHandlerImpl.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass)); + } + + @Test + public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() + throws CameraAccessException { + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockCamera, times(1)).pausePreview(); + verify(mockResult, times(1)).success(null); + } + + @Test + public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException() + throws CameraAccessException { + doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview(); + + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockResult, times(1)).error("CameraAccess", null, null); + } + + @Test + public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() { + handler.onMethodCall(new MethodCall("resumePreview", null), mockResult); + + verify(mockCamera, times(1)).resumePreview(); + verify(mockResult, times(1)).success(null); + } +} 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/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 new file mode 100644 index 000000000000..957b57a66435 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -0,0 +1,332 @@ +// Copyright 2013 The Flutter 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.features.resolution; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import io.flutter.plugins.camera.CameraProperties; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class ResolutionFeatureTest { + private static final String cameraName = "1"; + private CamcorderProfile mockProfileLowLegacy; + private EncoderProfiles mockProfileLow; + private MockedStatic mockedStaticProfile; + + @Before + @SuppressWarnings("deprecation") + public void beforeLegacy() { + mockedStaticProfile = mockStatic(CamcorderProfile.class); + mockProfileLowLegacy = mock(CamcorderProfile.class); + CamcorderProfile mockProfileLegacy = mock(CamcorderProfile.class); + + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfileLegacy); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfileLegacy); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfileLegacy); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfileLegacy); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfileLegacy); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfileLegacy); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLowLegacy); + } + + public void before() { + mockProfileLow = mock(EncoderProfiles.class); + EncoderProfiles mockProfile = mock(EncoderProfiles.class); + EncoderProfiles.VideoProfile mockVideoProfile = mock(EncoderProfiles.VideoProfile.class); + List mockVideoProfilesList = List.of(mockVideoProfile); + + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLow); + + when(mockProfile.getVideoProfiles()).thenReturn(mockVideoProfilesList); + when(mockVideoProfile.getHeight()).thenReturn(100); + when(mockVideoProfile.getWidth()).thenReturn(100); + } + + @After + public void after() { + mockedStaticProfile.reset(); + mockedStaticProfile.close(); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnInitialValueWhenNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); + } + + @Test + public void getValue_shouldEchoSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + resolutionFeature.setValue(ResolutionPreset.high); + + assertEquals(ResolutionPreset.high, resolutionFeature.getValue()); + } + + @Test + public void checkIsSupport_returnsTrue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertTrue(resolutionFeature.checkIsSupported()); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThroughLegacy() { + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + assertEquals( + mockProfileLowLegacy, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + 1, ResolutionPreset.max)); + } + + @Config(minSdk = 31) + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + assertEquals( + mockProfileLow, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + 1, ResolutionPreset.max)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMaxLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Config(minSdk = 31) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHighLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMediumLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); + } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLowLegacy() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); + } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { + before(); + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)); + } +} 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 58f17cb758bf..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 @@ -36,6 +36,7 @@ public class DeviceOrientationManagerTest { private DeviceOrientationManager deviceOrientationManager; @Before + @SuppressWarnings("deprecation") public void before() { mockActivity = mock(Activity.class); mockDartMessenger = mock(DartMessenger.class); @@ -61,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 @@ -80,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/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 new file mode 100644 index 000000000000..6cc58ee823d9 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -0,0 +1,227 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.media; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.media.CamcorderProfile; +import android.media.EncoderProfiles; +import android.media.MediaRecorder; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class MediaRecorderBuilderTest { + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void ctor_testLegacy() { + MediaRecorderBuilder builder = + new MediaRecorderBuilder(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P), ""); + + assertNotNull(builder); + } + + @Config(minSdk = 31) + @Test + public void ctor_test() { + MediaRecorderBuilder builder = + new MediaRecorderBuilder(CamcorderProfile.getAll("0", CamcorderProfile.QUALITY_1080P), ""); + + assertNotNull(builder); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabledLegacy() throws IOException { + CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Config(minSdk = 31) + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + List mockVideoProfiles = + List.of(mock(EncoderProfiles.VideoProfile.class)); + List mockAudioProfiles = + List.of(mock(EncoderProfiles.AudioProfile.class)); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + when(recorderProfile.getVideoProfiles()).thenReturn(mockVideoProfiles); + when(recorderProfile.getAudioProfiles()).thenReturn(mockAudioProfiles); + + MediaRecorder recorder = builder.build(); + + EncoderProfiles.VideoProfile videoProfile = mockVideoProfiles.get(0); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.getRecommendedFileFormat()); + inOrder.verify(recorder).setVideoEncoder(videoProfile.getCodec()); + inOrder.verify(recorder).setVideoEncodingBitRate(videoProfile.getBitrate()); + inOrder.verify(recorder).setVideoFrameRate(videoProfile.getFrameRate()); + inOrder.verify(recorder).setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Config(minSdk = 31) + @Test(expected = IndexOutOfBoundsException.class) + public void build_shouldThrowExceptionWithoutVideoOrAudioProfiles() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + } + + @Config(maxSdk = 30) + @SuppressWarnings("deprecation") + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabledLegacy() throws IOException { + CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setAudioEncoder(recorderProfile.audioCodec); + inOrder.verify(recorder).setAudioEncodingBitRate(recorderProfile.audioBitRate); + inOrder.verify(recorder).setAudioSamplingRate(recorderProfile.audioSampleRate); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Config(minSdk = 31) + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { + EncoderProfiles recorderProfile = mock(EncoderProfiles.class); + List mockVideoProfiles = + List.of(mock(EncoderProfiles.VideoProfile.class)); + List mockAudioProfiles = + List.of(mock(EncoderProfiles.AudioProfile.class)); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + when(recorderProfile.getVideoProfiles()).thenReturn(mockVideoProfiles); + when(recorderProfile.getAudioProfiles()).thenReturn(mockAudioProfiles); + + MediaRecorder recorder = builder.build(); + + EncoderProfiles.VideoProfile videoProfile = mockVideoProfiles.get(0); + EncoderProfiles.AudioProfile audioProfile = mockAudioProfiles.get(0); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.getRecommendedFileFormat()); + inOrder.verify(recorder).setAudioEncoder(audioProfile.getCodec()); + inOrder.verify(recorder).setAudioEncodingBitRate(audioProfile.getBitrate()); + inOrder.verify(recorder).setAudioSamplingRate(audioProfile.getSampleRate()); + inOrder.verify(recorder).setVideoEncoder(videoProfile.getCodec()); + inOrder.verify(recorder).setVideoEncodingBitRate(videoProfile.getBitrate()); + inOrder.verify(recorder).setVideoFrameRate(videoProfile.getFrameRate()); + inOrder.verify(recorder).setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + private CamcorderProfile getEmptyCamcorderProfile() { + try { + Constructor constructor = + CamcorderProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class, int.class, int.class, + int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } +} 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/android/src/test/resources/robolectric.properties b/packages/camera/camera_android/android/src/test/resources/robolectric.properties new file mode 100644 index 000000000000..90fbd74370a7 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=30 \ No newline at end of file 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..476d65373723 --- /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.12' + 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/android_alarm_manager/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/android_alarm_manager/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/camera/camera_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java 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/android_alarm_manager/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 similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/camera/camera_android/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/android_alarm_manager/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 similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/camera/camera_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/android_alarm_manager/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 similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/camera/camera_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/android_alarm_manager/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 similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/camera/camera_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/android_alarm_manager/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 similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/android_alarm_manager/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 similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/camera/camera_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/android_alarm_manager/example/android/app/src/main/res/values/styles.xml b/packages/camera/camera_android/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/android_alarm_manager/example/android/app/src/main/res/values/styles.xml rename to packages/camera/camera_android/example/android/app/src/main/res/values/styles.xml diff --git a/packages/connectivity/connectivity/example/android/build.gradle b/packages/camera/camera_android/example/android/build.gradle similarity index 100% rename from packages/connectivity/connectivity/example/android/build.gradle rename to packages/camera/camera_android/example/android/build.gradle 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/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/android_alarm_manager/example/android/settings.gradle b/packages/camera/camera_android/example/android/settings.gradle similarity index 100% rename from packages/android_alarm_manager/example/android/settings.gradle rename to packages/camera/camera_android/example/android/settings.gradle 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..99029fcac605 --- /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..5a7a79c8d96c --- /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..1dbdabb7d11c --- /dev/null +++ b/packages/camera/camera_android/example/lib/main.dart @@ -0,0 +1,1103 @@ +// Copyright 2013 The Flutter Authors. 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( + mainAxisAlignment: MainAxisAlignment.start, + 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + 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..64e69405041c --- /dev/null +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: camera_example +description: Demonstrates how to use the camera plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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: ../ + 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..b02929ec8c8c --- /dev/null +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -0,0 +1,590 @@ +// Copyright 2013 The Flutter Authors. 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(), + }, + ) + // 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 + .catchError( + (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..581780f0d87b --- /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 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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..ca1f245019a8 --- /dev/null +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -0,0 +1,1095 @@ +// Copyright 2013 The Flutter Authors. 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', + details: null, + ) + }, + ); + + // 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_android/test/method_channel_mock.dart b/packages/camera/camera_android/test/method_channel_mock.dart new file mode 100644 index 000000000000..413c10633cc1 --- /dev/null +++ b/packages/camera/camera_android/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_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/android_intent/AUTHORS b/packages/camera/camera_avfoundation/AUTHORS similarity index 100% rename from packages/android_intent/AUTHORS rename to packages/camera/camera_avfoundation/AUTHORS diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md new file mode 100644 index 000000000000..9bab2ec375c1 --- /dev/null +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -0,0 +1,19 @@ +## NEXT + +* 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/android_intent/LICENSE b/packages/camera/camera_avfoundation/LICENSE similarity index 100% rename from packages/android_intent/LICENSE rename to packages/camera/camera_avfoundation/LICENSE 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..51eab634c84b --- /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/local_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist 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/battery/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/battery/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 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/battery/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/battery/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/connectivity/connectivity/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/camera/camera_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/battery/battery/example/ios/Runner/AppDelegate.h b/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/battery/battery/example/ios/Runner/AppDelegate.h rename to packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.h diff --git a/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.m b/packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/AppDelegate.m rename to packages/camera/camera_avfoundation/example/ios/Runner/AppDelegate.m 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/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/camera/camera_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/battery/battery/example/ios/Runner/Base.lproj/Main.storyboard b/packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/battery/battery/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/camera/camera_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard 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_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m new file mode 100644 index 000000000000..89f40307933c --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m @@ -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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; + +@interface CameraCaptureSessionQueueRaceConditionTests : XCTestCase +@end + +@implementation CameraCaptureSessionQueueRaceConditionTests + +- (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + + XCTestExpectation *disposeExpectation = + [self expectationWithDescription:@"dispose's result block must be called"]; + XCTestExpectation *createExpectation = + [self expectationWithDescription:@"create's result block must be called"]; + FlutterMethodCall *disposeCall = [FlutterMethodCall methodCallWithMethodName:@"dispose" + arguments:nil]; + FlutterMethodCall *createCall = [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; + // Mimic a dispose call followed by a create call, which can be triggered by slightly dragging the + // home bar, causing the app to be inactive, and immediately regain active. + [camera handleMethodCall:disposeCall + result:^(id _Nullable result) { + [disposeExpectation 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:` + // API will cause a crash. + XCTAssertNotNil(camera.captureSessionQueue, + @"captureSessionQueue must not be nil after create method. "); +} + +@end 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 81% rename from packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m index 27537e7ebdac..1b6ada564dd8 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m @@ -2,26 +2,12 @@ // 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 camera_avfoundation.Test; @import XCTest; @import AVFoundation; #import -// Mirrors FocusMode in camera.dart -typedef enum { - FocusModeAuto, - FocusModeLocked, -} FocusMode; - -@interface FLTCam : NSObject - -- (void)applyFocusMode; -- (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice; -- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y; -@end - @interface CameraFocusTests : XCTestCase @property(readonly, nonatomic) FLTCam *camera; @property(readonly, nonatomic) id mockDevice; @@ -51,7 +37,7 @@ - (void)testAutoFocusWithContinuousModeSupported_ShouldSetContinuousAutoFocus { [[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus]; // Run test - [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice]; + [_camera applyFocusMode:FLTFocusModeAuto onDevice:_mockDevice]; // Expect setFocusMode:AVCaptureFocusModeContinuousAutoFocus OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]); @@ -68,7 +54,7 @@ - (void)testAutoFocusWithContinuousModeNotSupported_ShouldSetAutoFocus { [[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; // Run test - [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice]; + [_camera applyFocusMode:FLTFocusModeAuto onDevice:_mockDevice]; // Expect setFocusMode:AVCaptureFocusModeAutoFocus OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeAutoFocus]); @@ -86,7 +72,7 @@ - (void)testAutoFocusWithNoModeSupported_ShouldSetNothing { [[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus]; // Run test - [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice]; + [_camera applyFocusMode:FLTFocusModeAuto onDevice:_mockDevice]; } - (void)testLockedFocusWithModeSupported_ShouldSetModeAutoFocus { @@ -99,7 +85,7 @@ - (void)testLockedFocusWithModeSupported_ShouldSetModeAutoFocus { [[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; // Run test - [_camera applyFocusMode:FocusModeLocked onDevice:_mockDevice]; + [_camera applyFocusMode:FLTFocusModeLocked onDevice:_mockDevice]; // Expect setFocusMode:AVCaptureFocusModeAutoFocus OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeAutoFocus]); @@ -116,7 +102,7 @@ - (void)testLockedFocusWithModeNotSupported_ShouldSetNothing { [[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus]; // Run test - [_camera applyFocusMode:FocusModeLocked onDevice:_mockDevice]; + [_camera applyFocusMode:FLTFocusModeLocked onDevice:_mockDevice]; } - (void)testSetFocusPointWithResult_SetsFocusPointOfInterest { @@ -128,11 +114,11 @@ - (void)testSetFocusPointWithResult_SetsFocusPointOfInterest { [_camera setValue:_mockDevice forKey:@"captureDevice"]; // Run test - [_camera - setFocusPointWithResult:^void(id _Nullable result) { - } - x:1 - y:1]; + [_camera setFocusPointWithResult:[[FLTThreadSafeFlutterResult alloc] + initWithResult:^(id _Nullable result){ + }] + x:1 + y:1]; // Verify the focus point of interest has been set OCMVerify([_mockDevice setFocusPointOfInterest:CGPointMake(1, 1)]); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m new file mode 100644 index 000000000000..bd20134db561 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; +@import AVFoundation; +#import +#import "MockFLTThreadSafeFlutterResult.h" + +@interface CameraMethodChannelTests : XCTestCase +@end + +@implementation CameraMethodChannelTests + +- (void)testCreate_ShouldCallResultOnMainThread { + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + // Set up mocks for initWithCameraName method + id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) + .andReturn([AVCaptureInput alloc]); + + id avCaptureSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([avCaptureSessionMock alloc]).andReturn(avCaptureSessionMock); + OCMStub([avCaptureSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + MockFLTThreadSafeFlutterResult *resultObject = + [[MockFLTThreadSafeFlutterResult alloc] initWithExpectation:expectation]; + + // Set up method call + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"create" + arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; + + [camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + + // Verify the result + NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; + XCTAssertNotNil(dictionaryResult); + XCTAssert([[dictionaryResult allKeys] containsObject:@"cameraId"]); +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m new file mode 100644 index 000000000000..29fc325dffc0 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m @@ -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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; +@import Flutter; + +#import + +@interface CameraOrientationTests : XCTestCase +@end + +@implementation CameraOrientationTests + +- (void)testOrientationNotifications { + id mockMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil messenger:mockMessenger]; + + [mockMessenger setExpectationOrderMatters:YES]; + + [self rotate:UIDeviceOrientationPortraitUpsideDown + expectedChannelOrientation:@"portraitDown" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationPortrait + expectedChannelOrientation:@"portraitUp" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeRight + expectedChannelOrientation:@"landscapeLeft" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + [self rotate:UIDeviceOrientationLandscapeLeft + expectedChannelOrientation:@"landscapeRight" + cameraPlugin:cameraPlugin + messenger:mockMessenger]; + + OCMReject([mockMessenger sendOnChannel:[OCMArg any] message:[OCMArg any]]); + + // No notification when flat. + [cameraPlugin + orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceUp]]; + // No notification when facedown. + [cameraPlugin + orientationChanged:[self createMockNotificationForOrientation:UIDeviceOrientationFaceDown]]; + + OCMVerifyAll(mockMessenger); +} + +- (void)testOrientationUpdateMustBeOnCaptureSessionQueue { + XCTestExpectation *queueExpectation = [self + expectationWithDescription:@"Orientation update must happen on the capture session queue"]; + + CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + const char *captureSessionQueueSpecific = "capture_session_queue"; + dispatch_queue_set_specific(camera.captureSessionQueue, captureSessionQueueSpecific, + (void *)captureSessionQueueSpecific, NULL); + FLTCam *mockCam = OCMClassMock([FLTCam class]); + camera.camera = mockCam; + OCMStub([mockCam setDeviceOrientation:UIDeviceOrientationLandscapeLeft]) + .andDo(^(NSInvocation *invocation) { + if (dispatch_get_specific(captureSessionQueueSpecific)) { + [queueExpectation fulfill]; + } + }); + + [camera orientationChanged: + [self createMockNotificationForOrientation:UIDeviceOrientationLandscapeLeft]]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)rotate:(UIDeviceOrientation)deviceOrientation + expectedChannelOrientation:(NSString *)channelOrientation + cameraPlugin:(CameraPlugin *)cameraPlugin + messenger:(NSObject *)messenger { + XCTestExpectation *orientationExpectation = [self expectationWithDescription:channelOrientation]; + + OCMExpect([messenger + sendOnChannel:[OCMArg any] + message:[OCMArg checkWithBlock:^BOOL(NSData *data) { + NSObject *codec = [FlutterStandardMethodCodec sharedInstance]; + FlutterMethodCall *methodCall = [codec decodeMethodCall:data]; + [orientationExpectation fulfill]; + return + [methodCall.method isEqualToString:@"orientation_changed"] && + [methodCall.arguments isEqualToDictionary:@{@"orientation" : channelOrientation}]; + }]]); + + [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:deviceOrientation]]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (NSNotification *)createMockNotificationForOrientation:(UIDeviceOrientation)deviceOrientation { + UIDevice *mockDevice = OCMClassMock([UIDevice class]); + OCMStub([mockDevice orientation]).andReturn(deviceOrientation); + + return [NSNotification notificationWithName:@"orientation_test" object:mockDevice]; +} + +@end 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_avfoundation/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPreviewPauseTests.m new file mode 100644 index 000000000000..1dfc90b27f1b --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; +@import AVFoundation; +#import +#import "MockFLTThreadSafeFlutterResult.h" + +@interface CameraPreviewPauseTests : XCTestCase +@end + +@implementation CameraPreviewPauseTests + +- (void)testPausePreviewWithResult_shouldPausePreview { + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera pausePreviewWithResult:resultObject]; + XCTAssertTrue(camera.isPreviewPaused); +} + +- (void)testResumePreviewWithResult_shouldResumePreview { + FLTCam *camera = [[FLTCam alloc] init]; + MockFLTThreadSafeFlutterResult *resultObject = [[MockFLTThreadSafeFlutterResult alloc] init]; + + [camera resumePreviewWithResult:resultObject]; + XCTAssertFalse(camera.isPreviewPaused); +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m new file mode 100644 index 000000000000..18c01e599907 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m @@ -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 camera_avfoundation.Test; +@import AVFoundation; +@import XCTest; + +@interface CameraPropertiesTests : XCTestCase + +@end + +@implementation CameraPropertiesTests + +#pragma mark - flash mode tests + +- (void)testFLTGetFLTFlashModeForString { + XCTAssertEqual(FLTFlashModeOff, FLTGetFLTFlashModeForString(@"off")); + XCTAssertEqual(FLTFlashModeAuto, FLTGetFLTFlashModeForString(@"auto")); + XCTAssertEqual(FLTFlashModeAlways, FLTGetFLTFlashModeForString(@"always")); + XCTAssertEqual(FLTFlashModeTorch, FLTGetFLTFlashModeForString(@"torch")); + XCTAssertThrows(FLTGetFLTFlashModeForString(@"unkwown")); +} + +- (void)testFLTGetAVCaptureFlashModeForFLTFlashMode { + XCTAssertEqual(AVCaptureFlashModeOff, FLTGetAVCaptureFlashModeForFLTFlashMode(FLTFlashModeOff)); + XCTAssertEqual(AVCaptureFlashModeAuto, FLTGetAVCaptureFlashModeForFLTFlashMode(FLTFlashModeAuto)); + XCTAssertEqual(AVCaptureFlashModeOn, FLTGetAVCaptureFlashModeForFLTFlashMode(FLTFlashModeAlways)); + XCTAssertEqual(-1, FLTGetAVCaptureFlashModeForFLTFlashMode(FLTFlashModeTorch)); +} + +#pragma mark - exposure mode tests + +- (void)testFLTGetStringForFLTExposureMode { + XCTAssertEqualObjects(@"auto", FLTGetStringForFLTExposureMode(FLTExposureModeAuto)); + XCTAssertEqualObjects(@"locked", FLTGetStringForFLTExposureMode(FLTExposureModeLocked)); + XCTAssertThrows(FLTGetStringForFLTExposureMode(-1)); +} + +- (void)testFLTGetFLTExposureModeForString { + XCTAssertEqual(FLTExposureModeAuto, FLTGetFLTExposureModeForString(@"auto")); + XCTAssertEqual(FLTExposureModeLocked, FLTGetFLTExposureModeForString(@"locked")); + XCTAssertThrows(FLTGetFLTExposureModeForString(@"unknown")); +} + +#pragma mark - focus mode tests + +- (void)testFLTGetStringForFLTFocusMode { + XCTAssertEqualObjects(@"auto", FLTGetStringForFLTFocusMode(FLTFocusModeAuto)); + XCTAssertEqualObjects(@"locked", FLTGetStringForFLTFocusMode(FLTFocusModeLocked)); + XCTAssertThrows(FLTGetStringForFLTFocusMode(-1)); +} + +- (void)testFLTGetFLTFocusModeForString { + XCTAssertEqual(FLTFocusModeAuto, FLTGetFLTFocusModeForString(@"auto")); + XCTAssertEqual(FLTFocusModeLocked, FLTGetFLTFocusModeForString(@"locked")); + XCTAssertThrows(FLTGetFLTFocusModeForString(@"unknown")); +} + +#pragma mark - resolution preset tests + +- (void)testFLTGetFLTResolutionPresetForString { + XCTAssertEqual(FLTResolutionPresetVeryLow, FLTGetFLTResolutionPresetForString(@"veryLow")); + XCTAssertEqual(FLTResolutionPresetLow, FLTGetFLTResolutionPresetForString(@"low")); + XCTAssertEqual(FLTResolutionPresetMedium, FLTGetFLTResolutionPresetForString(@"medium")); + XCTAssertEqual(FLTResolutionPresetHigh, FLTGetFLTResolutionPresetForString(@"high")); + XCTAssertEqual(FLTResolutionPresetVeryHigh, FLTGetFLTResolutionPresetForString(@"veryHigh")); + XCTAssertEqual(FLTResolutionPresetUltraHigh, FLTGetFLTResolutionPresetForString(@"ultraHigh")); + XCTAssertEqual(FLTResolutionPresetMax, FLTGetFLTResolutionPresetForString(@"max")); + XCTAssertThrows(FLTGetFLTFlashModeForString(@"unknown")); +} + +#pragma mark - video format tests + +- (void)testFLTGetVideoFormatFromString { + XCTAssertEqual(kCVPixelFormatType_32BGRA, FLTGetVideoFormatFromString(@"bgra8888")); + XCTAssertEqual(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + FLTGetVideoFormatFromString(@"yuv420")); + XCTAssertEqual(kCVPixelFormatType_32BGRA, FLTGetVideoFormatFromString(@"unknown")); +} + +#pragma mark - device orientation tests + +- (void)testFLTGetUIDeviceOrientationForString { + XCTAssertEqual(UIDeviceOrientationPortraitUpsideDown, + FLTGetUIDeviceOrientationForString(@"portraitDown")); + XCTAssertEqual(UIDeviceOrientationLandscapeRight, + FLTGetUIDeviceOrientationForString(@"landscapeLeft")); + XCTAssertEqual(UIDeviceOrientationLandscapeLeft, + FLTGetUIDeviceOrientationForString(@"landscapeRight")); + XCTAssertEqual(UIDeviceOrientationPortrait, FLTGetUIDeviceOrientationForString(@"portraitUp")); + XCTAssertThrows(FLTGetUIDeviceOrientationForString(@"unknown")); +} + +- (void)testFLTGetStringForUIDeviceOrientation { + XCTAssertEqualObjects(@"portraitDown", + FLTGetStringForUIDeviceOrientation(UIDeviceOrientationPortraitUpsideDown)); + XCTAssertEqualObjects(@"landscapeLeft", + FLTGetStringForUIDeviceOrientation(UIDeviceOrientationLandscapeRight)); + XCTAssertEqualObjects(@"landscapeRight", + FLTGetStringForUIDeviceOrientation(UIDeviceOrientationLandscapeLeft)); + XCTAssertEqualObjects(@"portraitUp", + FLTGetStringForUIDeviceOrientation(UIDeviceOrientationPortrait)); + XCTAssertEqualObjects(@"portraitUp", FLTGetStringForUIDeviceOrientation(-1)); +} + +@end 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_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m new file mode 100644 index 000000000000..8a7c34cc2731 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -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 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 + +@implementation FLTCamPhotoCaptureTests + +- (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError { + XCTestExpectation *errorExpectation = + [self expectationWithDescription: + @"Must send error to result if save photo delegate completes with error."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendError:error]).andDo(^(NSInvocation *invocation) { + [errorExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(nil, error); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath { + XCTestExpectation *pathExpectation = + [self expectationWithDescription: + @"Must send file path to result if save photo delegate completes with file path."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); + + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSString *filePath = @"test"; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendSuccessWithData:filePath]).andDo(^(NSInvocation *invocation) { + [pathExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(filePath, nil); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + [self waitForExpectationsWithTimeout:1 handler: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_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m new file mode 100644 index 000000000000..f7633591ccb6 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -0,0 +1,140 @@ +// Copyright 2013 The Flutter Authors. 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 + +@interface FLTSavePhotoDelegateTests : XCTestCase + +@end + +@implementation FLTSavePhotoDelegateTests + +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to capture photo."]; + + NSError *captureError = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(captureError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; + + [delegate handlePhotoCaptureResultWithError:captureError + photoDataProvider:^NSData * { + return nil; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to write file."]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + + NSError *ioError = [NSError errorWithDomain:@"IOError" + code:0 + userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}]; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(ioError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; + + // 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:ioError]]) + .andReturn(NO); + [delegate handlePhotoCaptureResultWithError:nil + photoDataProvider:^NSData * { + return mockData; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path if success to write file."]; + + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(filePath, path); + [completionExpectation fulfill]; + }]; + + // 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]]) + .andReturn(YES); + + [delegate handlePhotoCaptureResultWithError:nil + photoDataProvider:^NSData * { + return mockData; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue { + XCTestExpectation *dataProviderQueueExpectation = + [self expectationWithDescription:@"Data provider must run on io queue."]; + XCTestExpectation *writeFileQueueExpectation = + [self expectationWithDescription:@"File writing must run on io queue"]; + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path if success to write file."]; + + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + const char *ioQueueSpecific = "io_queue_specific"; + dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); + + // 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]]) + .andDo(^(NSInvocation *invocation) { + if (dispatch_get_specific(ioQueueSpecific)) { + [writeFileQueueExpectation fulfill]; + } + }) + .andReturn(YES); + + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + [completionExpectation fulfill]; + }]; + + [delegate handlePhotoCaptureResultWithError:nil + photoDataProvider:^NSData * { + if (dispatch_get_specific(ioQueueSpecific)) { + [dataProviderQueueExpectation fulfill]; + } + return mockData; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end 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_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..8685f3fd610b --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.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. + +#ifndef MockFLTThreadSafeFlutterResult_h +#define MockFLTThreadSafeFlutterResult_h + +/** + * Extends FLTThreadSafeFlutterResult to give tests the ability to wait on the result and + * read the received result. + */ +@interface MockFLTThreadSafeFlutterResult : FLTThreadSafeFlutterResult +@property(readonly, nonatomic, nonnull) XCTestExpectation *expectation; +@property(nonatomic, nullable) id receivedResult; + +/** + * Initializes the MockFLTThreadSafeFlutterResult with an expectation. + * + * The expectation is fullfilled when a result is called allowing tests to await the result in an + * asynchronous manner. + */ +- (nonnull instancetype)initWithExpectation:(nonnull XCTestExpectation *)expectation; +@end + +#endif /* MockFLTThreadSafeFlutterResult_h */ diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..d3d7b6ac15b3 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockFLTThreadSafeFlutterResult.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 camera_avfoundation; +@import XCTest; + +#import "MockFLTThreadSafeFlutterResult.h" + +@implementation MockFLTThreadSafeFlutterResult + +- (instancetype)initWithExpectation:(XCTestExpectation *)expectation { + self = [super init]; + _expectation = expectation; + return self; +} + +- (void)sendSuccessWithData:(id)data { + self.receivedResult = data; + [self.expectation fulfill]; +} + +- (void)sendSuccess { + self.receivedResult = nil; + [self.expectation fulfill]; +} +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueUtilsTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueUtilsTests.m new file mode 100644 index 000000000000..a9fc7396bb99 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueUtilsTests.m @@ -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 camera_avfoundation; +@import XCTest; + +@interface QueueUtilsTests : XCTestCase + +@end + +@implementation QueueUtilsTests + +- (void)testShouldStayOnMainQueueIfCalledFromMainQueue { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Block must be run on the main queue."]; + FLTEnsureToRunOnMainQueue(^{ + if (NSThread.isMainThread) { + [expectation fulfill]; + } + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (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), ^{ + FLTEnsureToRunOnMainQueue(^{ + if (NSThread.isMainThread) { + [expectation fulfill]; + } + }); + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end 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_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m new file mode 100644 index 000000000000..e7460de6977e --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera_avfoundation; +@import XCTest; +#import + +@interface ThreadSafeEventChannelTests : XCTestCase +@end + +@implementation ThreadSafeEventChannelTests + +- (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { + FlutterEventChannel *mockEventChannel = OCMClassMock([FlutterEventChannel class]); + FLTThreadSafeEventChannel *threadSafeEventChannel = + [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; + + XCTestExpectation *mainThreadExpectation = + [self expectationWithDescription:@"setStreamHandler must be called on the main thread"]; + XCTestExpectation *mainThreadCompletionExpectation = + [self expectationWithDescription: + @"setStreamHandler's completion block must be called on the main thread"]; + OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [mainThreadExpectation fulfill]; + } + }); + + [threadSafeEventChannel setStreamHandler:nil + completion:^{ + if (NSThread.isMainThread) { + [mainThreadCompletionExpectation fulfill]; + } + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread { + FlutterEventChannel *mockEventChannel = OCMClassMock([FlutterEventChannel class]); + FLTThreadSafeEventChannel *threadSafeEventChannel = + [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; + + XCTestExpectation *mainThreadExpectation = + [self expectationWithDescription:@"setStreamHandler must be called on the main thread"]; + XCTestExpectation *mainThreadCompletionExpectation = + [self expectationWithDescription: + @"setStreamHandler's completion block must be called on the main thread"]; + OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [mainThreadExpectation fulfill]; + } + }); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [threadSafeEventChannel setStreamHandler:nil + completion:^{ + if (NSThread.isMainThread) { + [mainThreadCompletionExpectation fulfill]; + } + }]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeFlutterResultTests.m new file mode 100644 index 000000000000..b8de19ce4ab5 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeFlutterResultTests.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 camera_avfoundation; +@import XCTest; + +@interface ThreadSafeFlutterResultTests : XCTestCase +@end + +@implementation ThreadSafeFlutterResultTests +- (void)testAsyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult *threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccess]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testSyncSendSuccess_ShouldCallResultOnMainThread { + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult *threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert(NSThread.isMainThread); + [expectation fulfill]; + }]; + [threadSafeFlutterResult sendSuccess]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testSendNotImplemented_ShouldSendNotImplementedToFlutterResult { + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult *threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterMethodNotImplemented.class]); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendNotImplemented]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testSendErrorDetails_ShouldSendErrorToFlutterResult { + NSString *errorCode = @"errorCode"; + NSString *errorMessage = @"message"; + NSString *errorDetails = @"error details"; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult *threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError *error = (FlutterError *)result; + XCTAssertEqualObjects(error.code, errorCode); + XCTAssertEqualObjects(error.message, errorMessage); + XCTAssertEqualObjects(error.details, errorDetails); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendErrorWithCode:errorCode message:errorMessage details:errorDetails]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testSendNSError_ShouldSendErrorToFlutterResult { + NSError *originalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:404 userInfo:nil]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult *threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssert([result isKindOfClass:FlutterError.class]); + FlutterError *error = (FlutterError *)result; + NSString *constructedErrorCode = + [NSString stringWithFormat:@"Error %d", (int)originalError.code]; + XCTAssertEqualObjects(error.code, constructedErrorCode); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendError:originalError]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testSendResult_ShouldSendResultToFlutterResult { + NSString *resultData = @"resultData"; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; + + FLTThreadSafeFlutterResult *threadSafeFlutterResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:^(id _Nullable result) { + XCTAssertEqualObjects(result, resultData); + [expectation fulfill]; + }]; + dispatch_queue_t dispatchQueue = dispatch_queue_create("test dispatchqueue", NULL); + dispatch_async(dispatchQueue, ^{ + [threadSafeFlutterResult sendSuccessWithData:resultData]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m new file mode 100644 index 000000000000..ce1b641cef6f --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeMethodChannelTests.m @@ -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 camera_avfoundation; +@import XCTest; +#import + +@interface ThreadSafeMethodChannelTests : XCTestCase +@end + +@implementation ThreadSafeMethodChannelTests + +- (void)testInvokeMethod_shouldStayOnMainThreadIfCalledFromMainThread { + FlutterMethodChannel *mockMethodChannel = OCMClassMock([FlutterMethodChannel class]); + FLTThreadSafeMethodChannel *threadSafeMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel]; + + XCTestExpectation *mainThreadExpectation = + [self expectationWithDescription:@"invokeMethod must be called on the main thread"]; + + OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [mainThreadExpectation fulfill]; + } + }); + + [threadSafeMethodChannel invokeMethod:@"foo" arguments:nil]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testInvokeMethod__shouldDispatchToMainThreadIfCalledFromBackgroundThread { + FlutterMethodChannel *mockMethodChannel = OCMClassMock([FlutterMethodChannel class]); + FLTThreadSafeMethodChannel *threadSafeMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:mockMethodChannel]; + + XCTestExpectation *mainThreadExpectation = + [self expectationWithDescription:@"invokeMethod must be called on the main thread"]; + + OCMStub([mockMethodChannel invokeMethod:[OCMArg any] arguments:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [mainThreadExpectation fulfill]; + } + }); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [threadSafeMethodChannel invokeMethod:@"foo" arguments:nil]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m new file mode 100644 index 000000000000..31f196ffdb9e --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeTextureRegistryTests.m @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. 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 XCTest; +#import + +@interface ThreadSafeTextureRegistryTests : XCTestCase +@end + +@implementation ThreadSafeTextureRegistryTests + +- (void)testShouldStayOnMainThreadIfCalledFromMainThread { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + FLTThreadSafeTextureRegistry *threadSafeTextureRegistry = + [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry]; + + XCTestExpectation *registerTextureExpectation = + [self expectationWithDescription:@"registerTexture must be called on the main thread"]; + XCTestExpectation *unregisterTextureExpectation = + [self expectationWithDescription:@"unregisterTexture must be called on the main thread"]; + XCTestExpectation *textureFrameAvailableExpectation = + [self expectationWithDescription:@"textureFrameAvailable must be called on the main thread"]; + XCTestExpectation *registerTextureCompletionExpectation = + [self expectationWithDescription: + @"registerTexture's completion block must be called on the main thread"]; + + OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [registerTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [unregisterTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [textureFrameAvailableExpectation fulfill]; + } + }); + + NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); + [threadSafeTextureRegistry registerTexture:anyTexture + completion:^(int64_t textureId) { + if (NSThread.isMainThread) { + [registerTextureCompletionExpectation fulfill]; + } + }]; + [threadSafeTextureRegistry textureFrameAvailable:0]; + [threadSafeTextureRegistry unregisterTexture:0]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testShouldDispatchToMainThreadIfCalledFromBackgroundThread { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + FLTThreadSafeTextureRegistry *threadSafeTextureRegistry = + [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:mockTextureRegistry]; + + XCTestExpectation *registerTextureExpectation = + [self expectationWithDescription:@"registerTexture must be called on the main thread"]; + XCTestExpectation *unregisterTextureExpectation = + [self expectationWithDescription:@"unregisterTexture must be called on the main thread"]; + XCTestExpectation *textureFrameAvailableExpectation = + [self expectationWithDescription:@"textureFrameAvailable must be called on the main thread"]; + XCTestExpectation *registerTextureCompletionExpectation = + [self expectationWithDescription: + @"registerTexture's completion block must be called on the main thread"]; + + OCMStub([mockTextureRegistry registerTexture:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [registerTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry unregisterTexture:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [unregisterTextureExpectation fulfill]; + } + }); + + OCMStub([mockTextureRegistry textureFrameAvailable:0]).andDo(^(NSInvocation *invocation) { + if (NSThread.isMainThread) { + [textureFrameAvailableExpectation fulfill]; + } + }); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSObject *anyTexture = OCMProtocolMock(@protocol(FlutterTexture)); + [threadSafeTextureRegistry registerTexture:anyTexture + completion:^(int64_t textureId) { + if (NSThread.isMainThread) { + [registerTextureCompletionExpectation fulfill]; + } + }]; + [threadSafeTextureRegistry textureFrameAvailable:0]; + [threadSafeTextureRegistry unregisterTexture:0]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end 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..5a7a79c8d96c --- /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..1dbdabb7d11c --- /dev/null +++ b/packages/camera/camera_avfoundation/example/lib/main.dart @@ -0,0 +1,1103 @@ +// Copyright 2013 The Flutter Authors. 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( + mainAxisAlignment: MainAxisAlignment.start, + 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // 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, + mainAxisSize: MainAxisSize.max, + 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, + mainAxisSize: MainAxisSize.max, + 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..78927fc70d76 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: camera_example +description: Demonstrates how to use the camera plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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: ../ + 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/connectivity/connectivity/example/test_driver/integration_test.dart b/packages/camera/camera_avfoundation/example/test_driver/integration_test.dart similarity index 100% rename from packages/connectivity/connectivity/example/test_driver/integration_test.dart rename to packages/camera/camera_avfoundation/example/test_driver/integration_test.dart diff --git a/packages/android_alarm_manager/.gitkeep b/packages/camera/camera_avfoundation/ios/Assets/.gitkeep similarity index 100% rename from packages/android_alarm_manager/.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_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m new file mode 100644 index 000000000000..cb19c0909158 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.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 "CameraPlugin.h" +#import "CameraPlugin_Test.h" + +@import AVFoundation; + +#import "CameraPermissionUtils.h" +#import "CameraProperties.h" +#import "FLTCam.h" +#import "FLTThreadSafeEventChannel.h" +#import "FLTThreadSafeFlutterResult.h" +#import "FLTThreadSafeMethodChannel.h" +#import "FLTThreadSafeTextureRegistry.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_avfoundation" + binaryMessenger:[registrar messenger]]; + CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures] + messenger:[registrar messenger]]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)initWithRegistry:(NSObject *)registry + messenger:(NSObject *)messenger { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; + _messenger = messenger; + _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + + [self initDeviceEventMethodChannel]; + [self startOrientationListener]; + return self; +} + +- (void)initDeviceEventMethodChannel { + FlutterMethodChannel *methodChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.flutter.io/camera_avfoundation/fromPlatform" + binaryMessenger:_messenger]; + _deviceEventMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; +} + +- (void)startOrientationListener { + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +} + +- (void)orientationChanged:(NSNotification *)note { + UIDevice *device = note.object; + UIDeviceOrientation orientation = device.orientation; + + if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown) { + // Do not change when oriented flat. + return; + } + + dispatch_async(self.captureSessionQueue, ^{ + // `FLTCam::setDeviceOrientation` must be called on capture session queue. + [self.camera setDeviceOrientation:orientation]; + // `CameraPlugin::sendDeviceOrientation` can be called on any queue. + [self sendDeviceOrientation:orientation]; + }); +} + +- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { + [_deviceEventMethodChannel + invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : FLTGetStringForUIDeviceOrientation(orientation)}]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + // Invoke the plugin on another dispatch queue to avoid blocking the UI. + dispatch_async(_captureSessionQueue, ^{ + FLTThreadSafeFlutterResult *threadSafeResult = + [[FLTThreadSafeFlutterResult alloc] initWithResult:result]; + + [self handleMethodCallAsync:call result:threadSafeResult]; + }); +} + +- (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:discoveryDevices + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; + NSArray *devices = discoverySession.devices; + NSMutableArray *> *reply = + [[NSMutableArray alloc] initWithCapacity:devices.count]; + for (AVCaptureDevice *device in devices) { + NSString *lensFacing; + switch ([device position]) { + case AVCaptureDevicePositionBack: + lensFacing = @"back"; + break; + case AVCaptureDevicePositionFront: + lensFacing = @"front"; + break; + case AVCaptureDevicePositionUnspecified: + lensFacing = @"external"; + break; + } + [reply addObject:@{ + @"name" : [device uniqueID], + @"lensFacing" : lensFacing, + @"sensorOrientation" : @90, + }]; + } + [result sendSuccessWithData:reply]; + } else { + [result sendNotImplemented]; + } + } else if ([@"create" isEqualToString:call.method]) { + [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; + if ([@"initialize" isEqualToString:call.method]) { + NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]); + [_camera setVideoFormat:FLTGetVideoFormatFromString(videoFormatValue)]; + + __weak CameraPlugin *weakSelf = self; + _camera.onFrameAvailable = ^{ + if (![weakSelf.camera isPreviewPaused]) { + [weakSelf.registry textureFrameAvailable:cameraId]; + } + }; + FlutterMethodChannel *methodChannel = [FlutterMethodChannel + methodChannelWithName: + [NSString stringWithFormat:@"plugins.flutter.io/camera_avfoundation/camera%lu", + (unsigned long)cameraId] + binaryMessenger:_messenger]; + FLTThreadSafeMethodChannel *threadSafeMethodChannel = + [[FLTThreadSafeMethodChannel alloc] initWithMethodChannel:methodChannel]; + _camera.methodChannel = threadSafeMethodChannel; + [threadSafeMethodChannel + invokeMethod:@"initialized" + arguments:@{ + @"previewWidth" : @(_camera.previewSize.width), + @"previewHeight" : @(_camera.previewSize.height), + @"exposureMode" : FLTGetStringForFLTExposureMode([_camera exposureMode]), + @"focusMode" : FLTGetStringForFLTFocusMode([_camera focusMode]), + @"exposurePointSupported" : + @([_camera.captureDevice isExposurePointOfInterestSupported]), + @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), + }]; + [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; + [_camera start]; + [result sendSuccess]; + } else if ([@"takePicture" isEqualToString:call.method]) { + if (@available(iOS 10.0, *)) { + [_camera captureToFile:result]; + } else { + [result sendNotImplemented]; + } + } else if ([@"dispose" isEqualToString:call.method]) { + [_registry unregisterTexture:cameraId]; + [_camera close]; + [result sendSuccess]; + } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { + [self.camera setUpCaptureSessionForAudio]; + [result sendSuccess]; + } else if ([@"startVideoRecording" isEqualToString:call.method]) { + [_camera startVideoRecordingWithResult:result]; + } else if ([@"stopVideoRecording" isEqualToString:call.method]) { + [_camera stopVideoRecordingWithResult:result]; + } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { + [_camera pauseVideoRecordingWithResult:result]; + } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { + [_camera resumeVideoRecordingWithResult:result]; + } else if ([@"getMaxZoomLevel" isEqualToString:call.method]) { + [_camera getMaxZoomLevelWithResult:result]; + } else if ([@"getMinZoomLevel" isEqualToString:call.method]) { + [_camera getMinZoomLevelWithResult:result]; + } else if ([@"setZoomLevel" isEqualToString:call.method]) { + CGFloat zoom = ((NSNumber *)argsMap[@"zoom"]).floatValue; + [_camera setZoomLevel:zoom Result:result]; + } else if ([@"setFlashMode" isEqualToString:call.method]) { + [_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposureMode" isEqualToString:call.method]) { + [_camera setExposureModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposurePoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setExposurePointWithResult:result x:x y:y]; + } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { + [result sendSuccessWithData:@(_camera.captureDevice.minExposureTargetBias)]; + } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { + [result sendSuccessWithData:@(_camera.captureDevice.maxExposureTargetBias)]; + } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { + [result sendSuccessWithData:@(0.0)]; + } else if ([@"setExposureOffset" isEqualToString:call.method]) { + [_camera setExposureOffsetWithResult:result + offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; + } else if ([@"lockCaptureOrientation" isEqualToString:call.method]) { + [_camera lockCaptureOrientationWithResult:result orientation:call.arguments[@"orientation"]]; + } else if ([@"unlockCaptureOrientation" isEqualToString:call.method]) { + [_camera unlockCaptureOrientationWithResult:result]; + } else if ([@"setFocusMode" isEqualToString:call.method]) { + [_camera setFocusModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setFocusPoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setFocusPointWithResult:result x:x y:y]; + } else if ([@"pausePreview" isEqualToString:call.method]) { + [_camera pausePreviewWithResult:result]; + } else if ([@"resumePreview" isEqualToString:call.method]) { + [_camera resumePreviewWithResult:result]; + } else { + [result sendNotImplemented]; + } + } +} + +- (void)handleCreateMethodCall:(FlutterMethodCall *)call + result:(FLTThreadSafeFlutterResult *)result { + // Create FLTCam only if granted camera access (and audio access if audio is enabled) + FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + 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) { + if (error) { + [result sendFlutterError:error]; + } else { + [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + } + }); + } else { + [self createCameraOnSessionQueueWithCreateMethodCall:call result:result]; + } + } + }); +} + +- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result { + dispatch_async(self.captureSessionQueue, ^{ + 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:self.captureSessionQueue + error:&error]; + + if (error) { + [result sendError:error]; + } else { + if (self.camera) { + [self.camera close]; + } + self.camera = cam; + [self.registry registerTexture:cam + completion:^(int64_t textureId) { + [result sendSuccessWithData:@{ + @"cameraId" : @(textureId), + }]; + }]; + } + }); +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.modulemap new file mode 100644 index 000000000000..abdad1ab575c --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.modulemap @@ -0,0 +1,20 @@ +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" + header "FLTSavePhotoDelegate_Test.h" + header "FLTThreadSafeEventChannel.h" + header "FLTThreadSafeFlutterResult.h" + header "FLTThreadSafeMethodChannel.h" + header "FLTThreadSafeTextureRegistry.h" + header "QueueUtils.h" + } +} diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.h new file mode 100644 index 000000000000..77a758d8dea3 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin_Test.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. + +// This header is available in the Test module. Import via "@import camera_avfoundation.Test;" + +#import "CameraPlugin.h" +#import "FLTCam.h" +#import "FLTThreadSafeFlutterResult.h" + +/// Methods exposed for unit testing. +@interface CameraPlugin () + +/// All FLTCam's state access and capture session related operations should be on run on this queue. +@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; + +/// An internal camera object that manages camera's state and performs camera operations. +@property(nonatomic, strong) FLTCam *camera; + +/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. +- (instancetype)initWithRegistry:(NSObject *)registry + messenger:(NSObject *)messenger + NS_DESIGNATED_INITIALIZER; + +/// Hide the default public constructor. +- (instancetype)init NS_UNAVAILABLE; + +/// Handles `FlutterMethodCall`s and ensures result is send on the main dispatch queue. +/// +/// @param call The method call command object. +/// @param result A wrapper around the `FlutterResult` callback which ensures the callback is called +/// on the main dispatch queue. +- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FLTThreadSafeFlutterResult *)result; + +/// Called by the @c NSNotificationManager each time the device's orientation is changed. +/// +/// @param notification @c NSNotification instance containing a reference to the `UIDevice` object +/// 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_avfoundation/ios/Classes/CameraProperties.h b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h new file mode 100644 index 000000000000..aee4d643f64f --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h @@ -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 AVFoundation; +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - flash mode + +/** + * Represents camera's flash mode. Mirrors `FlashMode` enum in flash_mode.dart. + */ +typedef NS_ENUM(NSInteger, FLTFlashMode) { + FLTFlashModeOff, + FLTFlashModeAuto, + FLTFlashModeAlways, + FLTFlashModeTorch, +}; + +/** + * Gets FLTFlashMode from its string representation. + * @param mode a string representation of the FLTFlashMode. + */ +extern FLTFlashMode FLTGetFLTFlashModeForString(NSString *mode); + +/** + * Gets AVCaptureFlashMode from FLTFlashMode. + * @param mode flash mode. + */ +extern AVCaptureFlashMode FLTGetAVCaptureFlashModeForFLTFlashMode(FLTFlashMode mode); + +#pragma mark - exposure mode + +/** + * Represents camera's exposure mode. Mirrors ExposureMode in camera.dart. + */ +typedef NS_ENUM(NSInteger, FLTExposureMode) { + FLTExposureModeAuto, + FLTExposureModeLocked, +}; + +/** + * Gets a string representation of exposure mode. + * @param mode exposure mode + */ +extern NSString *FLTGetStringForFLTExposureMode(FLTExposureMode mode); + +/** + * Gets FLTExposureMode from its string representation. + * @param mode a string representation of the FLTExposureMode. + */ +extern FLTExposureMode FLTGetFLTExposureModeForString(NSString *mode); + +#pragma mark - focus mode + +/** + * Represents camera's focus mode. Mirrors FocusMode in camera.dart. + */ +typedef NS_ENUM(NSInteger, FLTFocusMode) { + FLTFocusModeAuto, + FLTFocusModeLocked, +}; + +/** + * Gets a string representation from FLTFocusMode. + * @param mode focus mode + */ +extern NSString *FLTGetStringForFLTFocusMode(FLTFocusMode mode); + +/** + * Gets FLTFocusMode from its string representation. + * @param mode a string representation of focus mode. + */ +extern FLTFocusMode FLTGetFLTFocusModeForString(NSString *mode); + +#pragma mark - device orientation + +/** + * Gets UIDeviceOrientation from its string representation. + */ +extern UIDeviceOrientation FLTGetUIDeviceOrientationForString(NSString *orientation); + +/** + * Gets a string representation of UIDeviceOrientation. + */ +extern NSString *FLTGetStringForUIDeviceOrientation(UIDeviceOrientation orientation); + +#pragma mark - resolution preset + +/** + * Represents camera's resolution present. Mirrors ResolutionPreset in camera.dart. + */ +typedef NS_ENUM(NSInteger, FLTResolutionPreset) { + FLTResolutionPresetVeryLow, + FLTResolutionPresetLow, + FLTResolutionPresetMedium, + FLTResolutionPresetHigh, + FLTResolutionPresetVeryHigh, + FLTResolutionPresetUltraHigh, + FLTResolutionPresetMax, +}; + +/** + * Gets FLTResolutionPreset from its string representation. + * @param preset a string representation of FLTResolutionPreset. + */ +extern FLTResolutionPreset FLTGetFLTResolutionPresetForString(NSString *preset); + +#pragma mark - video format + +/** + * Gets VideoFormat from its string representation. + */ +extern OSType FLTGetVideoFormatFromString(NSString *videoFormatString); + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m new file mode 100644 index 000000000000..e36f98af27f1 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m @@ -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. + +#import "CameraProperties.h" + +#pragma mark - flash mode + +FLTFlashMode FLTGetFLTFlashModeForString(NSString *mode) { + if ([mode isEqualToString:@"off"]) { + return FLTFlashModeOff; + } else if ([mode isEqualToString:@"auto"]) { + return FLTFlashModeAuto; + } else if ([mode isEqualToString:@"always"]) { + return FLTFlashModeAlways; + } else if ([mode isEqualToString:@"torch"]) { + return FLTFlashModeTorch; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown flash mode %@", mode] + }]; + @throw error; + } +} + +AVCaptureFlashMode FLTGetAVCaptureFlashModeForFLTFlashMode(FLTFlashMode mode) { + switch (mode) { + case FLTFlashModeOff: + return AVCaptureFlashModeOff; + case FLTFlashModeAuto: + return AVCaptureFlashModeAuto; + case FLTFlashModeAlways: + return AVCaptureFlashModeOn; + case FLTFlashModeTorch: + default: + return -1; + } +} + +#pragma mark - exposure mode + +NSString *FLTGetStringForFLTExposureMode(FLTExposureMode mode) { + switch (mode) { + case FLTExposureModeAuto: + return @"auto"; + case FLTExposureModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for exposure mode"] + }]; + @throw error; +} + +FLTExposureMode FLTGetFLTExposureModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return FLTExposureModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return FLTExposureModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown exposure mode %@", mode] + }]; + @throw error; + } +} + +#pragma mark - focus mode + +NSString *FLTGetStringForFLTFocusMode(FLTFocusMode mode) { + switch (mode) { + case FLTFocusModeAuto: + return @"auto"; + case FLTFocusModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for focus mode"] + }]; + @throw error; +} + +FLTFocusMode FLTGetFLTFocusModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return FLTFocusModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return FLTFocusModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown focus mode %@", mode] + }]; + @throw error; + } +} + +#pragma mark - device orientation + +UIDeviceOrientation FLTGetUIDeviceOrientationForString(NSString *orientation) { + if ([orientation isEqualToString:@"portraitDown"]) { + return UIDeviceOrientationPortraitUpsideDown; + } else if ([orientation isEqualToString:@"landscapeLeft"]) { + return UIDeviceOrientationLandscapeRight; + } else if ([orientation isEqualToString:@"landscapeRight"]) { + return UIDeviceOrientationLandscapeLeft; + } else if ([orientation isEqualToString:@"portraitUp"]) { + return UIDeviceOrientationPortrait; + } else { + NSError *error = [NSError + errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + [NSString stringWithFormat:@"Unknown device orientation %@", orientation] + }]; + @throw error; + } +} + +NSString *FLTGetStringForUIDeviceOrientation(UIDeviceOrientation orientation) { + switch (orientation) { + case UIDeviceOrientationPortraitUpsideDown: + return @"portraitDown"; + case UIDeviceOrientationLandscapeRight: + return @"landscapeLeft"; + case UIDeviceOrientationLandscapeLeft: + return @"landscapeRight"; + case UIDeviceOrientationPortrait: + default: + return @"portraitUp"; + }; +} + +#pragma mark - resolution preset + +FLTResolutionPreset FLTGetFLTResolutionPresetForString(NSString *preset) { + if ([preset isEqualToString:@"veryLow"]) { + return FLTResolutionPresetVeryLow; + } else if ([preset isEqualToString:@"low"]) { + return FLTResolutionPresetLow; + } else if ([preset isEqualToString:@"medium"]) { + return FLTResolutionPresetMedium; + } else if ([preset isEqualToString:@"high"]) { + return FLTResolutionPresetHigh; + } else if ([preset isEqualToString:@"veryHigh"]) { + return FLTResolutionPresetVeryHigh; + } else if ([preset isEqualToString:@"ultraHigh"]) { + return FLTResolutionPresetUltraHigh; + } else if ([preset isEqualToString:@"max"]) { + return FLTResolutionPresetMax; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown resolution preset %@", preset] + }]; + @throw error; + } +} + +#pragma mark - video format + +OSType FLTGetVideoFormatFromString(NSString *videoFormatString) { + if ([videoFormatString isEqualToString:@"bgra8888"]) { + return kCVPixelFormatType_32BGRA; + } else if ([videoFormatString isEqualToString:@"yuv420"]) { + return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; + } else { + NSLog(@"The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888"); + return kCVPixelFormatType_32BGRA; + } +} diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h new file mode 100644 index 000000000000..8a5dafaf8354 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -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. + +@import AVFoundation; +@import Foundation; +@import Flutter; + +#import "CameraProperties.h" +#import "FLTThreadSafeEventChannel.h" +#import "FLTThreadSafeFlutterResult.h" +#import "FLTThreadSafeMethodChannel.h" +#import "FLTThreadSafeTextureRegistry.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A class that manages camera's state and performs camera operations. + */ +@interface FLTCam : NSObject + +@property(readonly, nonatomic) AVCaptureDevice *captureDevice; +@property(readonly, nonatomic) CGSize previewSize; +@property(assign, nonatomic) BOOL isPreviewPaused; +@property(nonatomic, copy) void (^onFrameAvailable)(void); +@property(nonatomic) FLTThreadSafeMethodChannel *methodChannel; +@property(assign, nonatomic) FLTResolutionPreset resolutionPreset; +@property(assign, nonatomic) FLTExposureMode exposureMode; +@property(assign, nonatomic) FLTFocusMode focusMode; +@property(assign, nonatomic) FLTFlashMode flashMode; +// 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 + orientation:(UIDeviceOrientation)orientation + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + error:(NSError **)error; +- (void)start; +- (void)stop; +- (void)setDeviceOrientation:(UIDeviceOrientation)orientation; +- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)); +- (void)close; +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result + orientation:(NSString *)orientationStr; +- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; +- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; +- (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. + * + * If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use + * AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to + * AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor + * AVCaptureFocusModeAutoFocus are supported focus mode will not be set. + * If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use + * AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not + * be set. + * + * @param focusMode The focus mode that should be applied to the @captureDevice instance. + * @param captureDevice The AVCaptureDevice to which the @focusMode will be applied. + */ +- (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice; +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y; +- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y; +- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset; +- (void)startImageStreamWithMessenger:(NSObject *)messenger; +- (void)stopImageStream; +- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result; +- (void)setUpCaptureSessionForAudio; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m new file mode 100644 index 000000000000..f267604af419 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -0,0 +1,1093 @@ +// Copyright 2013 The Flutter Authors. 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 "FLTCam_Test.h" +#import "FLTSavePhotoDelegate.h" +#import "QueueUtils.h" + +@import CoreMotion; +#import + +@implementation FLTImageStreamHandler + +- (instancetype)initWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _captureSessionQueue = captureSessionQueue; + return self; +} + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + dispatch_async(self.captureSessionQueue, ^{ + self.eventSink = nil; + }); + return nil; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + dispatch_async(self.captureSessionQueue, ^{ + self.eventSink = events; + }); + return nil; +} +@end + +@interface FLTCam () + +@property(readonly, nonatomic) int64_t textureId; +@property BOOL enableAudio; +@property(nonatomic) FLTImageStreamHandler *imageStreamHandler; +@property(readonly, nonatomic) AVCaptureSession *captureSession; + +@property(readonly, nonatomic) AVCaptureInput *captureVideoInput; +/// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback. +/// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API. +@property(readwrite, nonatomic) CVPixelBufferRef latestPixelBuffer; +@property(readonly, nonatomic) CGSize captureSize; +@property(strong, nonatomic) AVAssetWriter *videoWriter; +@property(strong, nonatomic) AVAssetWriterInput *videoWriterInput; +@property(strong, nonatomic) AVAssetWriterInput *audioWriterInput; +@property(strong, nonatomic) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor; +@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput; +@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput; +@property(strong, nonatomic) NSString *videoRecordingPath; +@property(assign, nonatomic) BOOL isRecording; +@property(assign, nonatomic) BOOL isRecordingPaused; +@property(assign, nonatomic) BOOL videoIsDisconnected; +@property(assign, nonatomic) BOOL audioIsDisconnected; +@property(assign, nonatomic) BOOL isAudioSetup; + +/// Number of frames currently pending processing. +@property(assign, nonatomic) int streamingPendingFramesCount; + +/// Maximum number of frames pending processing. +@property(assign, nonatomic) int maxStreamingPendingFramesCount; + +@property(assign, nonatomic) UIDeviceOrientation lockedCaptureOrientation; +@property(assign, nonatomic) CMTime lastVideoSampleTime; +@property(assign, nonatomic) CMTime lastAudioSampleTime; +@property(assign, nonatomic) CMTime videoTimeOffset; +@property(assign, nonatomic) CMTime audioTimeOffset; +@property(nonatomic) CMMotionManager *motionManager; +@property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor; +/// All FLTCam's state access and capture session related operations should be on run on this queue. +@property(strong, nonatomic) dispatch_queue_t captureSessionQueue; +/// The queue on which `latestPixelBuffer` property is accessed. +/// To avoid unnecessary contention, do not access `latestPixelBuffer` on the `captureSessionQueue`. +@property(strong, nonatomic) dispatch_queue_t pixelBufferSynchronizationQueue; +/// The queue on which captured photos (not videos) are written to disk. +/// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation. +@property(strong, nonatomic) dispatch_queue_t photoIOQueue; +@property(assign, nonatomic) UIDeviceOrientation deviceOrientation; +@end + +@implementation FLTCam + +NSString *const errorMethod = @"error"; + +- (instancetype)initWithCameraName:(NSString *)cameraName + resolutionPreset:(NSString *)resolutionPreset + enableAudio:(BOOL)enableAudio + orientation:(UIDeviceOrientation)orientation + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + error:(NSError **)error { + return [self initWithCameraName:cameraName + resolutionPreset:resolutionPreset + enableAudio:enableAudio + orientation:orientation + captureSession:[[AVCaptureSession alloc] init] + captureSessionQueue:captureSessionQueue + error:error]; +} + +- (instancetype)initWithCameraName:(NSString *)cameraName + resolutionPreset:(NSString *)resolutionPreset + enableAudio:(BOOL)enableAudio + orientation:(UIDeviceOrientation)orientation + captureSession:(AVCaptureSession *)captureSession + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + error:(NSError **)error { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + @try { + _resolutionPreset = FLTGetFLTResolutionPresetForString(resolutionPreset); + } @catch (NSError *e) { + *error = e; + } + _enableAudio = enableAudio; + _captureSessionQueue = captureSessionQueue; + _pixelBufferSynchronizationQueue = + dispatch_queue_create("io.flutter.camera.pixelBufferSynchronizationQueue", NULL); + _photoIOQueue = dispatch_queue_create("io.flutter.camera.photoIOQueue", NULL); + _captureSession = captureSession; + _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName]; + _flashMode = _captureDevice.hasFlash ? FLTFlashModeAuto : FLTFlashModeOff; + _exposureMode = FLTExposureModeAuto; + _focusMode = FLTFocusModeAuto; + _lockedCaptureOrientation = UIDeviceOrientationUnknown; + _deviceOrientation = orientation; + _videoFormat = kCVPixelFormatType_32BGRA; + _inProgressSavePhotoDelegates = [NSMutableDictionary dictionary]; + + // To limit memory consumption, limit the number of frames pending processing. + // After some testing, 4 was determined to be the best maximum value. + // https://github.com/flutter/plugins/pull/4520#discussion_r766335637 + _maxStreamingPendingFramesCount = 4; + + NSError *localError = nil; + _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice + error:&localError]; + + if (localError) { + *error = localError; + return nil; + } + + _captureVideoOutput = [AVCaptureVideoDataOutput new]; + _captureVideoOutput.videoSettings = + @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)}; + [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES]; + [_captureVideoOutput setSampleBufferDelegate:self queue:captureSessionQueue]; + + AVCaptureConnection *connection = + [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports + output:_captureVideoOutput]; + + if ([_captureDevice position] == AVCaptureDevicePositionFront) { + connection.videoMirrored = YES; + } + + [_captureSession addInputWithNoConnections:_captureVideoInput]; + [_captureSession addOutputWithNoConnections:_captureVideoOutput]; + [_captureSession addConnection:connection]; + + if (@available(iOS 10.0, *)) { + _capturePhotoOutput = [AVCapturePhotoOutput new]; + [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; + [_captureSession addOutput:_capturePhotoOutput]; + } + _motionManager = [[CMMotionManager alloc] init]; + [_motionManager startAccelerometerUpdates]; + + [self setCaptureSessionPreset:_resolutionPreset]; + [self updateOrientation]; + + return self; +} + +- (void)start { + [_captureSession startRunning]; +} + +- (void)stop { + [_captureSession stopRunning]; +} + +- (void)setVideoFormat:(OSType)videoFormat { + _videoFormat = videoFormat; + _captureVideoOutput.videoSettings = + @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)}; +} + +- (void)setDeviceOrientation:(UIDeviceOrientation)orientation { + if (_deviceOrientation == orientation) { + return; + } + + _deviceOrientation = orientation; + [self updateOrientation]; +} + +- (void)updateOrientation { + if (_isRecording) { + return; + } + + UIDeviceOrientation orientation = (_lockedCaptureOrientation != UIDeviceOrientationUnknown) + ? _lockedCaptureOrientation + : _deviceOrientation; + + [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput]; + [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput]; +} + +- (void)updateOrientation:(UIDeviceOrientation)orientation + forCaptureOutput:(AVCaptureOutput *)captureOutput { + if (!captureOutput) { + return; + } + + AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo]; + if (connection && connection.isVideoOrientationSupported) { + connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation]; + } +} + +- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) { + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + if (_resolutionPreset == FLTResolutionPresetMax) { + [settings setHighResolutionPhotoEnabled:YES]; + } + + AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(_flashMode); + if (avFlashMode != -1) { + [settings setFlashMode:avFlashMode]; + } + NSError *error; + NSString *path = [self getTemporaryFilePathWithExtension:@"jpg" + subfolder:@"pictures" + prefix:@"CAP_" + error:error]; + if (error) { + [result sendError:error]; + return; + } + + FLTSavePhotoDelegate *savePhotoDelegate = [[FLTSavePhotoDelegate alloc] + initWithPath:path + ioQueue:self.photoIOQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + dispatch_async(self.captureSessionQueue, ^{ + // Dispatch back to capture session queue to delete reference. + // Retain cycle is broken after the dictionary entry is cleared. + // This is to keep the behavior with the previous `selfReference` approach in the + // FLTSavePhotoDelegate, where delegate is released only after capture completion. + [self.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)]; + }); + + if (error) { + [result sendError:error]; + } else { + NSAssert(path, @"Path must not be nil if no error."); + [result sendSuccessWithData:path]; + } + }]; + + NSAssert(dispatch_get_specific(FLTCaptureSessionQueueSpecific), + @"save photo delegate references must be updated on the capture session queue"); + self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate; + [self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate]; +} + +- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation: + (UIDeviceOrientation)deviceOrientation { + if (deviceOrientation == UIDeviceOrientationPortrait) { + return AVCaptureVideoOrientationPortrait; + } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) { + // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + return AVCaptureVideoOrientationLandscapeRight; + } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) { + // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape right the video orientation should be landscape left. + return AVCaptureVideoOrientationLandscapeLeft; + } else if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) { + return AVCaptureVideoOrientationPortraitUpsideDown; + } else { + return AVCaptureVideoOrientationPortrait; + } +} + +- (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension + subfolder:(NSString *)subfolder + prefix:(NSString *)prefix + error:(NSError *)error { + NSString *docDir = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + NSString *fileDir = + [[docDir stringByAppendingPathComponent:@"camera"] stringByAppendingPathComponent:subfolder]; + NSString *fileName = [prefix stringByAppendingString:[[NSUUID UUID] UUIDString]]; + NSString *file = + [[fileDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:extension]; + + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:fileDir]) { + [[NSFileManager defaultManager] createDirectoryAtPath:fileDir + withIntermediateDirectories:true + attributes:nil + error:&error]; + if (error) { + return nil; + } + } + + return file; +} + +- (void)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset { + switch (resolutionPreset) { + case FLTResolutionPresetMax: + case FLTResolutionPresetUltraHigh: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { + _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; + _previewSize = CGSizeMake(3840, 2160); + break; + } + if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) { + _captureSession.sessionPreset = AVCaptureSessionPresetHigh; + _previewSize = + CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width, + _captureDevice.activeFormat.highResolutionStillImageDimensions.height); + break; + } + case FLTResolutionPresetVeryHigh: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) { + _captureSession.sessionPreset = AVCaptureSessionPreset1920x1080; + _previewSize = CGSizeMake(1920, 1080); + break; + } + case FLTResolutionPresetHigh: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) { + _captureSession.sessionPreset = AVCaptureSessionPreset1280x720; + _previewSize = CGSizeMake(1280, 720); + break; + } + case FLTResolutionPresetMedium: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) { + _captureSession.sessionPreset = AVCaptureSessionPreset640x480; + _previewSize = CGSizeMake(640, 480); + break; + } + case FLTResolutionPresetLow: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) { + _captureSession.sessionPreset = AVCaptureSessionPreset352x288; + _previewSize = CGSizeMake(352, 288); + break; + } + default: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetLow]) { + _captureSession.sessionPreset = AVCaptureSessionPresetLow; + _previewSize = CGSizeMake(352, 288); + } else { + NSError *error = + [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + @"No capture session available for current capture session." + }]; + @throw error; + } + } +} + +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + if (output == _captureVideoOutput) { + CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CFRetain(newBuffer); + + __block CVPixelBufferRef previousPixelBuffer = nil; + // Use `dispatch_sync` to avoid unnecessary context switch under common non-contest scenarios; + // Under rare contest scenarios, it will not block for too long since the critical section is + // quite lightweight. + dispatch_sync(self.pixelBufferSynchronizationQueue, ^{ + previousPixelBuffer = self.latestPixelBuffer; + self.latestPixelBuffer = newBuffer; + }); + if (previousPixelBuffer) { + CFRelease(previousPixelBuffer); + } + if (_onFrameAvailable) { + _onFrameAvailable(); + } + } + if (!CMSampleBufferDataIsReady(sampleBuffer)) { + [_methodChannel invokeMethod:errorMethod + arguments:@"sample buffer is not ready. Skipping sample"]; + return; + } + if (_isStreamingImages) { + FlutterEventSink eventSink = _imageStreamHandler.eventSink; + if (eventSink && (self.streamingPendingFramesCount < self.maxStreamingPendingFramesCount)) { + self.streamingPendingFramesCount++; + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + // Must lock base address before accessing the pixel data + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer); + size_t imageHeight = CVPixelBufferGetHeight(pixelBuffer); + + NSMutableArray *planes = [NSMutableArray array]; + + const Boolean isPlanar = CVPixelBufferIsPlanar(pixelBuffer); + size_t planeCount; + if (isPlanar) { + planeCount = CVPixelBufferGetPlaneCount(pixelBuffer); + } else { + planeCount = 1; + } + + for (int i = 0; i < planeCount; i++) { + void *planeAddress; + size_t bytesPerRow; + size_t height; + size_t width; + + if (isPlanar) { + planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i); + bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i); + height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i); + width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i); + } else { + planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer); + bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer); + height = CVPixelBufferGetHeight(pixelBuffer); + width = CVPixelBufferGetWidth(pixelBuffer); + } + + NSNumber *length = @(bytesPerRow * height); + NSData *bytes = [NSData dataWithBytes:planeAddress length:length.unsignedIntegerValue]; + + NSMutableDictionary *planeBuffer = [NSMutableDictionary dictionary]; + planeBuffer[@"bytesPerRow"] = @(bytesPerRow); + planeBuffer[@"width"] = @(width); + planeBuffer[@"height"] = @(height); + planeBuffer[@"bytes"] = [FlutterStandardTypedData typedDataWithBytes:bytes]; + + [planes addObject:planeBuffer]; + } + // Lock the base address before accessing pixel data, and unlock it afterwards. + // Done accessing the `pixelBuffer` at this point. + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary]; + imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth]; + imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight]; + imageBuffer[@"format"] = @(_videoFormat); + imageBuffer[@"planes"] = planes; + imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]]; + Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]); + Float64 nsExposureDuration = 1000000000 * exposureDuration; + imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration]; + imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]]; + + dispatch_async(dispatch_get_main_queue(), ^{ + eventSink(imageBuffer); + }); + } + } + if (_isRecording && !_isRecordingPaused) { + if (_videoWriter.status == AVAssetWriterStatusFailed) { + [_methodChannel invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; + return; + } + + CFRetain(sampleBuffer); + CMTime currentSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); + + if (_videoWriter.status != AVAssetWriterStatusWriting) { + [_videoWriter startWriting]; + [_videoWriter startSessionAtSourceTime:currentSampleTime]; + } + + if (output == _captureVideoOutput) { + if (_videoIsDisconnected) { + _videoIsDisconnected = NO; + + if (_videoTimeOffset.value == 0) { + _videoTimeOffset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); + } else { + CMTime offset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); + _videoTimeOffset = CMTimeAdd(_videoTimeOffset, offset); + } + + return; + } + + _lastVideoSampleTime = currentSampleTime; + + CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset); + [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime]; + } else { + CMTime dur = CMSampleBufferGetDuration(sampleBuffer); + + if (dur.value > 0) { + currentSampleTime = CMTimeAdd(currentSampleTime, dur); + } + + if (_audioIsDisconnected) { + _audioIsDisconnected = NO; + + if (_audioTimeOffset.value == 0) { + _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + } else { + CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset); + } + + return; + } + + _lastAudioSampleTime = currentSampleTime; + + if (_audioTimeOffset.value != 0) { + CFRelease(sampleBuffer); + sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset]; + } + + [self newAudioSample:sampleBuffer]; + } + + CFRelease(sampleBuffer); + } +} + +- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset CF_RETURNS_RETAINED { + CMItemCount count; + CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); + CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count); + CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); + for (CMItemCount i = 0; i < count; i++) { + pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); + pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); + } + CMSampleBufferRef sout; + CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); + free(pInfo); + return sout; +} + +- (void)newVideoSample:(CMSampleBufferRef)sampleBuffer { + if (_videoWriter.status != AVAssetWriterStatusWriting) { + if (_videoWriter.status == AVAssetWriterStatusFailed) { + [_methodChannel invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; + } + return; + } + if (_videoWriterInput.readyForMoreMediaData) { + if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) { + [_methodChannel + invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", @"Unable to write to video input"]]; + } + } +} + +- (void)newAudioSample:(CMSampleBufferRef)sampleBuffer { + if (_videoWriter.status != AVAssetWriterStatusWriting) { + if (_videoWriter.status == AVAssetWriterStatusFailed) { + [_methodChannel invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; + } + return; + } + if (_audioWriterInput.readyForMoreMediaData) { + if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) { + [_methodChannel + invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", @"Unable to write to audio input"]]; + } + } +} + +- (void)close { + [_captureSession stopRunning]; + for (AVCaptureInput *input in [_captureSession inputs]) { + [_captureSession removeInput:input]; + } + for (AVCaptureOutput *output in [_captureSession outputs]) { + [_captureSession removeOutput:output]; + } +} + +- (void)dealloc { + if (_latestPixelBuffer) { + CFRelease(_latestPixelBuffer); + } + [_motionManager stopAccelerometerUpdates]; +} + +- (CVPixelBufferRef)copyPixelBuffer { + __block CVPixelBufferRef pixelBuffer = nil; + // Use `dispatch_sync` because `copyPixelBuffer` API requires synchronous return. + dispatch_sync(self.pixelBufferSynchronizationQueue, ^{ + pixelBuffer = self.latestPixelBuffer; + self.latestPixelBuffer = nil; + }); + return pixelBuffer; +} + +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { + if (!_isRecording) { + NSError *error; + _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" + subfolder:@"videos" + prefix:@"REC_" + error:error]; + if (error) { + [result sendError:error]; + return; + } + if (![self setupWriterForPath:_videoRecordingPath]) { + [result sendErrorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]; + return; + } + _isRecording = YES; + _isRecordingPaused = NO; + _videoTimeOffset = CMTimeMake(0, 1); + _audioTimeOffset = CMTimeMake(0, 1); + _videoIsDisconnected = NO; + _audioIsDisconnected = NO; + [result sendSuccess]; + } else { + [result sendErrorWithCode:@"Error" message:@"Video is already recording" details:nil]; + } +} + +- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { + if (_isRecording) { + _isRecording = NO; + + if (_videoWriter.status != AVAssetWriterStatusUnknown) { + [_videoWriter finishWritingWithCompletionHandler:^{ + if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { + [self updateOrientation]; + [result sendSuccessWithData:self->_videoRecordingPath]; + self->_videoRecordingPath = nil; + } else { + [result sendErrorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]; + } + }]; + } + } else { + NSError *error = + [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorResourceUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; + [result sendError:error]; + } +} + +- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { + _isRecordingPaused = YES; + _videoIsDisconnected = YES; + _audioIsDisconnected = YES; + [result sendSuccess]; +} + +- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { + _isRecordingPaused = NO; + [result sendSuccess]; +} + +- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result + orientation:(NSString *)orientationStr { + UIDeviceOrientation orientation; + @try { + orientation = FLTGetUIDeviceOrientationForString(orientationStr); + } @catch (NSError *e) { + [result sendError:e]; + return; + } + + if (_lockedCaptureOrientation != orientation) { + _lockedCaptureOrientation = orientation; + [self updateOrientation]; + } + + [result sendSuccess]; +} + +- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result { + _lockedCaptureOrientation = UIDeviceOrientationUnknown; + [self updateOrientation]; + [result sendSuccess]; +} + +- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { + FLTFlashMode mode; + @try { + mode = FLTGetFLTFlashModeForString(modeStr); + } @catch (NSError *e) { + [result sendError:e]; + return; + } + if (mode == FLTFlashModeTorch) { + if (!_captureDevice.hasTorch) { + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not support torch mode" + details:nil]; + return; + } + if (!_captureDevice.isTorchAvailable) { + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Torch mode is currently not available" + details:nil]; + return; + } + if (_captureDevice.torchMode != AVCaptureTorchModeOn) { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setTorchMode:AVCaptureTorchModeOn]; + [_captureDevice unlockForConfiguration]; + } + } else { + if (!_captureDevice.hasFlash) { + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not have flash capabilities" + details:nil]; + return; + } + AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(mode); + if (![_capturePhotoOutput.supportedFlashModes + containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { + [result sendErrorWithCode:@"setFlashModeFailed" + message:@"Device does not support this specific flash mode" + details:nil]; + return; + } + if (_captureDevice.torchMode != AVCaptureTorchModeOff) { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setTorchMode:AVCaptureTorchModeOff]; + [_captureDevice unlockForConfiguration]; + } + } + _flashMode = mode; + [result sendSuccess]; +} + +- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { + FLTExposureMode mode; + @try { + mode = FLTGetFLTExposureModeForString(modeStr); + } @catch (NSError *e) { + [result sendError:e]; + return; + } + _exposureMode = mode; + [self applyExposureMode]; + [result sendSuccess]; +} + +- (void)applyExposureMode { + [_captureDevice lockForConfiguration:nil]; + switch (_exposureMode) { + case FLTExposureModeLocked: + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + break; + case FLTExposureModeAuto: + if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + [_captureDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure]; + } else { + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + } + break; + } + [_captureDevice unlockForConfiguration]; +} + +- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { + FLTFocusMode mode; + @try { + mode = FLTGetFLTFocusModeForString(modeStr); + } @catch (NSError *e) { + [result sendError:e]; + return; + } + _focusMode = mode; + [self applyFocusMode]; + [result sendSuccess]; +} + +- (void)applyFocusMode { + [self applyFocusMode:_focusMode onDevice:_captureDevice]; +} + +- (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice { + [captureDevice lockForConfiguration:nil]; + switch (focusMode) { + case FLTFocusModeLocked: + if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { + [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; + } + break; + case FLTFocusModeAuto: + if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + } else if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { + [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; + } + break; + } + [captureDevice unlockForConfiguration]; +} + +- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result { + _isPreviewPaused = true; + [result sendSuccess]; +} + +- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result { + _isPreviewPaused = false; + [result sendSuccess]; +} + +- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation + x:(double)x + y:(double)y { + double oldX = x, oldY = y; + switch (orientation) { + case UIDeviceOrientationPortrait: // 90 ccw + y = 1 - oldX; + x = oldY; + break; + case UIDeviceOrientationPortraitUpsideDown: // 90 cw + x = 1 - oldY; + y = oldX; + break; + case UIDeviceOrientationLandscapeRight: // 180 + x = 1 - x; + y = 1 - y; + break; + case UIDeviceOrientationLandscapeLeft: + default: + // No rotation required + break; + } + return CGPointMake(x, y); +} + +- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { + if (!_captureDevice.isExposurePointOfInterestSupported) { + [result sendErrorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]; + return; + } + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposurePointOfInterest:[self getCGPointForCoordsWithOrientation:orientation + x:x + y:y]]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto exposure + [self applyExposureMode]; + [result sendSuccess]; +} + +- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y { + if (!_captureDevice.isFocusPointOfInterestSupported) { + [result sendErrorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]; + return; + } + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + [_captureDevice lockForConfiguration:nil]; + + [_captureDevice setFocusPointOfInterest:[self getCGPointForCoordsWithOrientation:orientation + x:x + y:y]]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto focus + [self applyFocusMode]; + [result sendSuccess]; +} + +- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposureTargetBias:offset completionHandler:nil]; + [_captureDevice unlockForConfiguration]; + [result sendSuccessWithData:@(offset)]; +} + +- (void)startImageStreamWithMessenger:(NSObject *)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_avfoundation/imageStream" + binaryMessenger:messenger]; + FLTThreadSafeEventChannel *threadSafeEventChannel = + [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel]; + + _imageStreamHandler = imageStreamHandler; + [threadSafeEventChannel setStreamHandler:_imageStreamHandler + completion:^{ + dispatch_async(self->_captureSessionQueue, ^{ + self.isStreamingImages = YES; + self.streamingPendingFramesCount = 0; + }); + }]; + } else { + [_methodChannel invokeMethod:errorMethod + arguments:@"Images from camera are already streaming!"]; + } +} + +- (void)stopImageStream { + if (_isStreamingImages) { + _isStreamingImages = NO; + _imageStreamHandler = nil; + } else { + [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"]; + } +} + +- (void)receivedImageStreamData { + self.streamingPendingFramesCount--; +} + +- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { + CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; + + [result sendSuccessWithData:[NSNumber numberWithFloat:maxZoomFactor]]; +} + +- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result { + CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; + [result sendSuccessWithData:[NSNumber numberWithFloat:minZoomFactor]]; +} + +- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result { + CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; + CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; + + if (maxAvailableZoomFactor < zoom || minAvailableZoomFactor > zoom) { + NSString *errorMessage = [NSString + stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", + minAvailableZoomFactor, maxAvailableZoomFactor]; + + [result sendErrorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil]; + return; + } + + NSError *error = nil; + if (![_captureDevice lockForConfiguration:&error]) { + [result sendError:error]; + return; + } + _captureDevice.videoZoomFactor = zoom; + [_captureDevice unlockForConfiguration]; + + [result sendSuccess]; +} + +- (CGFloat)getMinAvailableZoomFactor { + if (@available(iOS 11.0, *)) { + return _captureDevice.minAvailableVideoZoomFactor; + } else { + return 1.0; + } +} + +- (CGFloat)getMaxAvailableZoomFactor { + if (@available(iOS 11.0, *)) { + return _captureDevice.maxAvailableVideoZoomFactor; + } else { + return _captureDevice.activeFormat.videoMaxZoomFactor; + } +} + +- (BOOL)setupWriterForPath:(NSString *)path { + NSError *error = nil; + NSURL *outputURL; + if (path != nil) { + outputURL = [NSURL fileURLWithPath:path]; + } else { + return NO; + } + if (_enableAudio && !_isAudioSetup) { + [self setUpCaptureSessionForAudio]; + } + + _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL + fileType:AVFileTypeMPEG4 + error:&error]; + NSParameterAssert(_videoWriter); + if (error) { + [_methodChannel invokeMethod:errorMethod arguments:error.description]; + return NO; + } + + NSDictionary *videoSettings = [_captureVideoOutput + recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4]; + _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo + outputSettings:videoSettings]; + + _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput + sourcePixelBufferAttributes:@{ + (NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat) + }]; + + NSParameterAssert(_videoWriterInput); + + _videoWriterInput.expectsMediaDataInRealTime = YES; + + // Add the audio input + if (_enableAudio) { + AudioChannelLayout acl; + bzero(&acl, sizeof(acl)); + acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; + NSDictionary *audioOutputSettings = nil; + // Both type of audio inputs causes output video file to be corrupted. + audioOutputSettings = @{ + AVFormatIDKey : [NSNumber numberWithInt:kAudioFormatMPEG4AAC], + AVSampleRateKey : [NSNumber numberWithFloat:44100.0], + AVNumberOfChannelsKey : [NSNumber numberWithInt:1], + AVChannelLayoutKey : [NSData dataWithBytes:&acl length:sizeof(acl)], + }; + _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio + outputSettings:audioOutputSettings]; + _audioWriterInput.expectsMediaDataInRealTime = YES; + + [_videoWriter addInput:_audioWriterInput]; + [_audioOutput setSampleBufferDelegate:self queue:_captureSessionQueue]; + } + + if (_flashMode == FLTFlashModeTorch) { + [self.captureDevice lockForConfiguration:nil]; + [self.captureDevice setTorchMode:AVCaptureTorchModeOn]; + [self.captureDevice unlockForConfiguration]; + } + + [_videoWriter addInput:_videoWriterInput]; + + [_captureVideoOutput setSampleBufferDelegate:self queue:_captureSessionQueue]; + + return YES; +} + +- (void)setUpCaptureSessionForAudio { + NSError *error = nil; + // Create a device input with the device and add it to the session. + // Setup the audio input. + AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice + error:&error]; + if (error) { + [_methodChannel invokeMethod:errorMethod arguments:error.description]; + } + // Setup the audio output. + _audioOutput = [[AVCaptureAudioDataOutput alloc] init]; + + if ([_captureSession canAddInput:audioInput]) { + [_captureSession addInput:audioInput]; + + if ([_captureSession canAddOutput:_audioOutput]) { + [_captureSession addOutput:_audioOutput]; + _isAudioSetup = YES; + } else { + [_methodChannel invokeMethod:errorMethod + arguments:@"Unable to add Audio input/output to session capture"]; + _isAudioSetup = NO; + } + } +} +@end 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_avfoundation/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.h new file mode 100644 index 000000000000..40e4562e4483 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.h @@ -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 AVFoundation; +@import Foundation; + +#import "FLTThreadSafeFlutterResult.h" + +NS_ASSUME_NONNULL_BEGIN + +/// The completion handler block for save photo operations. +/// Can be called from either main queue or IO queue. +/// If success, `error` will be present and `path` will be nil. Otherewise, `error` will be nil and +/// `path` will be present. +/// @param path the path for successfully saved photo file. +/// @param error photo capture error or IO error. +typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSString *_Nullable path, + NSError *_Nullable error); + +/** + Delegate object that handles photo capture results. + */ +@interface FLTSavePhotoDelegate : NSObject + +/** + * Initialize a photo capture delegate. + * @param path the path for captured photo file. + * @param ioQueue the queue on which captured photos are written to disk. + * @param completionHandler The completion handler block for save photo operations. Can + * be called from either main queue or IO queue. + */ +- (instancetype)initWithPath:(NSString *)path + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m new file mode 100644 index 000000000000..1df1708c54e8 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate.m @@ -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 "FLTSavePhotoDelegate.h" +#import "FLTSavePhotoDelegate_Test.h" + +@interface FLTSavePhotoDelegate () +/// The file path for the captured photo. +@property(readonly, nonatomic) NSString *path; +/// The queue on which captured photos are written to disk. +@property(readonly, nonatomic) dispatch_queue_t ioQueue; +@end + +@implementation FLTSavePhotoDelegate + +- (instancetype)initWithPath:(NSString *)path + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _path = path; + _ioQueue = ioQueue; + _completionHandler = completionHandler; + return self; +} + +- (void)handlePhotoCaptureResultWithError:(NSError *)error + photoDataProvider:(NSData * (^)(void))photoDataProvider { + if (error) { + self.completionHandler(nil, error); + return; + } + dispatch_async(self.ioQueue, ^{ + NSData *data = photoDataProvider(); + NSError *ioError; + if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { + self.completionHandler(self.path, nil); + } else { + self.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 + resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings + bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings + error:(NSError *)error API_AVAILABLE(ios(10)) { + [self handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return [AVCapturePhotoOutput + JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer + previewPhotoSampleBuffer: + previewPhotoSampleBuffer]; + }]; +} +#pragma clang diagnostic pop + +- (void)captureOutput:(AVCapturePhotoOutput *)output + didFinishProcessingPhoto:(AVCapturePhoto *)photo + error:(NSError *)error API_AVAILABLE(ios(11.0)) { + [self handlePhotoCaptureResultWithError:error + photoDataProvider:^NSData * { + return [photo fileDataRepresentation]; + }]; +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate_Test.h new file mode 100644 index 000000000000..2d0d4f96be9d --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTSavePhotoDelegate_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 "FLTSavePhotoDelegate.h" + +/** + API exposed for unit tests. + */ +@interface FLTSavePhotoDelegate () + +/// The completion handler block for capture and save photo operations. +/// Can be called from either main queue or IO queue. +/// Exposed for unit tests to manually trigger the completion. +@property(readonly, nonatomic) FLTSavePhotoDelegateCompletionHandler completionHandler; + +/// Handler to write captured photo data into a file. +/// @param error the capture error. +/// @param photoDataProvider a closure that provides photo data. +- (void)handlePhotoCaptureResultWithError:(NSError *)error + photoDataProvider:(NSData * (^)(void))photoDataProvider; +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.h new file mode 100644 index 000000000000..ddfa75487a28 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A thread safe wrapper for FlutterEventChannel that can be called from any thread, by dispatching + * its underlying engine calls to the main thread. + */ +@interface FLTThreadSafeEventChannel : NSObject + +/** + * Creates a FLTThreadSafeEventChannel by wrapping a FlutterEventChannel object. + * @param channel The FlutterEventChannel object to be wrapped. + */ +- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel; + +/* + * Registers a handler on the main thread for stream setup requests from the Flutter side. + # The completion block runs on the main thread. + */ +- (void)setStreamHandler:(nullable NSObject *)handler + completion:(void (^)(void))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m new file mode 100644 index 000000000000..46941bb18dd6 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeEventChannel.m @@ -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 "FLTThreadSafeEventChannel.h" +#import "QueueUtils.h" + +@interface FLTThreadSafeEventChannel () +@property(nonatomic, strong) FlutterEventChannel *channel; +@end + +@implementation FLTThreadSafeEventChannel + +- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel { + self = [super init]; + if (self) { + _channel = channel; + } + return self; +} + +- (void)setStreamHandler:(NSObject *)handler + completion:(void (^)(void))completion { + FLTEnsureToRunOnMainQueue(^{ + [self.channel setStreamHandler:handler]; + completion(); + }); +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.h new file mode 100644 index 000000000000..6677505671a3 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.h @@ -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 + +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. + */ +@interface FLTThreadSafeFlutterResult : NSObject + +/** + * Gets the original FlutterResult object wrapped by this FLTThreadSafeFlutterResult instance. + */ +@property(readonly, nonatomic) FlutterResult flutterResult; + +/** + * Initializes with a FlutterResult object. + * @param result The FlutterResult object that the result will be given to. + */ +- (instancetype)initWithResult:(FlutterResult)result; + +/** + * Sends a successful result on the main thread without any data. + */ +- (void)sendSuccess; + +/** + * Sends a successful result on the main thread with data. + * @param data Result data that is send to the Flutter Dart side. + */ +- (void)sendSuccessWithData:(id)data; + +/** + * Sends an NSError as result on the main thread. + * @param error Error that will be send as FlutterError. + */ +- (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:(NSString *)code + message:(nullable NSString *)message + details:(nullable id)details; + +/** + * Sends FlutterMethodNotImplemented as result on the main thread. + */ +- (void)sendNotImplemented; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.m new file mode 100644 index 000000000000..ad125f7f32ed --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeFlutterResult.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 "FLTThreadSafeFlutterResult.h" +#import +#import "QueueUtils.h" + +@implementation FLTThreadSafeFlutterResult { +} + +- (id)initWithResult:(FlutterResult)result { + self = [super init]; + if (!self) { + return nil; + } + _flutterResult = result; + return self; +} + +- (void)sendSuccess { + [self send:nil]; +} + +- (void)sendSuccessWithData:(id)data { + [self send:data]; +} + +- (void)sendError:(NSError *)error { + [self sendErrorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] + message:error.localizedDescription + details:error.domain]; +} + +- (void)sendErrorWithCode:(NSString *)code + message:(NSString *_Nullable)message + details:(id _Nullable)details { + FlutterError *flutterError = [FlutterError errorWithCode:code message:message details:details]; + [self send:flutterError]; +} + +- (void)sendFlutterError:(FlutterError *)flutterError { + [self send:flutterError]; +} + +- (void)sendNotImplemented { + [self send:FlutterMethodNotImplemented]; +} + +/** + * Sends result to flutterResult on the main thread. + */ +- (void)send:(id _Nullable)result { + FLTEnsureToRunOnMainQueue(^{ + self.flutterResult(result); + }); +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.h new file mode 100644 index 000000000000..0f6611db03ce --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.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. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A thread safe wrapper for FlutterMethodChannel that can be called from any thread, by dispatching + * its underlying engine calls to the main thread. + */ +@interface FLTThreadSafeMethodChannel : NSObject + +/** + * Creates a FLTThreadSafeMethodChannel by wrapping a FlutterMethodChannel object. + * @param channel The FlutterMethodChannel object to be wrapped. + */ +- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel; + +/** + * Invokes the specified flutter method on the main thread with the specified arguments. + */ +- (void)invokeMethod:(NSString *)method arguments:(nullable id)arguments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m new file mode 100644 index 000000000000..5b29b70ee432 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeMethodChannel.m @@ -0,0 +1,28 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTThreadSafeMethodChannel.h" +#import "QueueUtils.h" + +@interface FLTThreadSafeMethodChannel () +@property(nonatomic, strong) FlutterMethodChannel *channel; +@end + +@implementation FLTThreadSafeMethodChannel + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _channel = channel; + } + return self; +} + +- (void)invokeMethod:(NSString *)method arguments:(id)arguments { + FLTEnsureToRunOnMainQueue(^{ + [self.channel invokeMethod:method arguments:arguments]; + }); +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.h b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.h new file mode 100644 index 000000000000..030e2dbc7818 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A thread safe wrapper for FlutterTextureRegistry that can be called from any thread, by + * dispatching its underlying engine calls to the main thread. + */ +@interface FLTThreadSafeTextureRegistry : NSObject + +/** + * Creates a FLTThreadSafeTextureRegistry by wrapping an object conforming to + * FlutterTextureRegistry. + * @param registry The FlutterTextureRegistry object to be wrapped. + */ +- (instancetype)initWithTextureRegistry:(NSObject *)registry; + +/** + * Registers a `FlutterTexture` on the main thread for usage in Flutter and returns an id that can + * be used to reference that texture when calling into Flutter with channels. + * + * On success the completion block completes with the pointer to the registered texture, else with + * 0. The completion block runs on the main thread. + */ +- (void)registerTexture:(NSObject *)texture + completion:(void (^)(int64_t))completion; + +/** + * Notifies the Flutter engine on the main thread that the given texture has been updated. + */ +- (void)textureFrameAvailable:(int64_t)textureId; + +/** + * Notifies the Flutter engine on the main thread to unregister a `FlutterTexture` that has been + * previously registered with `registerTexture:`. + * @param textureId The result that was previously returned from `registerTexture:`. + */ +- (void)unregisterTexture:(int64_t)textureId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.m new file mode 100644 index 000000000000..349b89fdc117 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTThreadSafeTextureRegistry.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 "FLTThreadSafeTextureRegistry.h" +#import "QueueUtils.h" + +@interface FLTThreadSafeTextureRegistry () +@property(nonatomic, strong) NSObject *registry; +@end + +@implementation FLTThreadSafeTextureRegistry + +- (instancetype)initWithTextureRegistry:(NSObject *)registry { + self = [super init]; + if (self) { + _registry = registry; + } + return self; +} + +- (void)registerTexture:(NSObject *)texture + completion:(void (^)(int64_t))completion { + FLTEnsureToRunOnMainQueue(^{ + completion([self.registry registerTexture:texture]); + }); +} + +- (void)textureFrameAvailable:(int64_t)textureId { + FLTEnsureToRunOnMainQueue(^{ + [self.registry textureFrameAvailable:textureId]; + }); +} + +- (void)unregisterTexture:(int64_t)textureId { + FLTEnsureToRunOnMainQueue(^{ + [self.registry unregisterTexture:textureId]; + }); +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.h b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.h new file mode 100644 index 000000000000..a7e22da716d0 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +/// Queue-specific context data to be associated with the capture session queue. +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. +/// @param block the block to be run on the main queue. +extern void FLTEnsureToRunOnMainQueue(dispatch_block_t block); + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.m b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.m new file mode 100644 index 000000000000..1fd54cd52cb3 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/QueueUtils.m @@ -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 "QueueUtils.h" + +const char *FLTCaptureSessionQueueSpecific = "capture_session_queue"; + +void FLTEnsureToRunOnMainQueue(dispatch_block_t block) { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), block); + } else { + block(); + } +} diff --git a/packages/camera/camera_avfoundation/ios/Classes/camera_avfoundation-umbrella.h b/packages/camera/camera_avfoundation/ios/Classes/camera_avfoundation-umbrella.h new file mode 100644 index 000000000000..f8464aaae3dc --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/Classes/camera_avfoundation-umbrella.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 + +FOUNDATION_EXPORT double cameraVersionNumber; +FOUNDATION_EXPORT const unsigned char cameraVersionString[]; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation.podspec b/packages/camera/camera_avfoundation/ios/camera_avfoundation.podspec new file mode 100644 index 000000000000..27f569c8b9be --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'camera_avfoundation' + s.version = '0.0.1' + s.summary = 'Flutter Camera' + s.description = <<-DESC +A Flutter plugin to use the camera from your Flutter app. + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/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' } +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..d4f986074671 --- /dev/null +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -0,0 +1,595 @@ +// Copyright 2013 The Flutter Authors. 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(), + }, + ) + // 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 + .catchError( + (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..adb0bf8dbafb --- /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+2 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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..67adcfab81f2 --- /dev/null +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -0,0 +1,1095 @@ +// Copyright 2013 The Flutter Authors. 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', + details: null, + ) + }, + ); + + // 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 195e142fe10f..9cc0b14dafbd 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,34 @@ +## NEXT + +* 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. + +## 2.1.4 + +* Removes dependency on `meta`. + +## 2.1.3 + +* Update to use the `verify` method introduced in platform_plugin_interface 2.1.0. + +## 2.1.2 + +* Adopts new analysis options and fixes all violations. + ## 2.1.1 * Add web-relevant docs to platform interface code. diff --git a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart index 3ec66dd54894..6fab99b3d694 100644 --- a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart +++ b/packages/camera/camera_platform_interface/lib/camera_platform_interface.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. +/// Expose XFile +export 'package:cross_file/cross_file.dart'; + export 'src/events/camera_event.dart'; export 'src/events/device_event.dart'; export 'src/platform_interface/camera_platform.dart'; export 'src/types/types.dart'; - -/// Expose XFile -export 'package:cross_file/cross_file.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart index 591d5a336356..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,7 @@ // 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'; @@ -22,14 +22,15 @@ import '../../camera_platform_interface.dart'; /// See below for examples: `CameraClosingEvent`, `CameraErrorEvent`... /// These events are more semantic and more pleasant to use than raw generics. /// They can be (and in fact, are) filtered by the `instanceof`-operator. +@immutable abstract class CameraEvent { - /// The ID of the Camera this event is associated to. - final int cameraId; - /// Build a Camera Event, that relates a `cameraId`. /// /// The `cameraId` is the ID of the camera that triggered the event. - CameraEvent(this.cameraId) : assert(cameraId != null); + const CameraEvent(this.cameraId) : assert(cameraId != null); + + /// The ID of the Camera this event is associated to. + final int cameraId; @override bool operator ==(Object other) => @@ -44,30 +45,12 @@ abstract class CameraEvent { /// An event fired when the camera has finished initializing. class CameraInitializedEvent extends CameraEvent { - /// The width of the preview in pixels. - final double previewWidth; - - /// The height of the preview in pixels. - final double previewHeight; - - /// The default exposure mode - final ExposureMode exposureMode; - - /// The default focus mode - final FocusMode focusMode; - - /// Whether setting exposure points is supported. - final bool exposurePointSupported; - - /// Whether setting focus points is supported. - final bool focusPointSupported; - /// Build a CameraInitialized event triggered from the camera represented by /// `cameraId`. /// /// The `previewWidth` represents the width of the generated preview in pixels. /// The `previewHeight` represents the height of the generated preview in pixels. - CameraInitializedEvent( + const CameraInitializedEvent( int cameraId, this.previewWidth, this.previewHeight, @@ -80,17 +63,36 @@ class CameraInitializedEvent extends CameraEvent { /// Converts the supplied [Map] to an instance of the [CameraInitializedEvent] /// class. CameraInitializedEvent.fromJson(Map json) - : previewWidth = json['previewWidth'], - previewHeight = json['previewHeight'], - exposureMode = deserializeExposureMode(json['exposureMode']), - exposurePointSupported = json['exposurePointSupported'] ?? false, - focusMode = deserializeFocusMode(json['focusMode']), - focusPointSupported = json['focusPointSupported'] ?? false, - super(json['cameraId']); + : previewWidth = json['previewWidth']! as double, + previewHeight = json['previewHeight']! as double, + exposureMode = deserializeExposureMode(json['exposureMode']! as String), + exposurePointSupported = + (json['exposurePointSupported'] as bool?) ?? false, + focusMode = deserializeFocusMode(json['focusMode']! as String), + focusPointSupported = (json['focusPointSupported'] as bool?) ?? false, + super(json['cameraId']! as int); + + /// The width of the preview in pixels. + final double previewWidth; + + /// The height of the preview in pixels. + final double previewHeight; + + /// The default exposure mode + final ExposureMode exposureMode; + + /// The default focus mode + final FocusMode focusMode; + + /// Whether setting exposure points is supported. + final bool exposurePointSupported; + + /// Whether setting focus points is supported. + final bool focusPointSupported; /// Converts the [CameraInitializedEvent] instance into a [Map] instance that /// can be serialized to JSON. - Map toJson() => { + Map toJson() => { 'cameraId': cameraId, 'previewWidth': previewWidth, 'previewHeight': previewHeight, @@ -114,30 +116,25 @@ 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. class CameraResolutionChangedEvent extends CameraEvent { - /// The capture width in pixels. - final double captureWidth; - - /// The capture height in pixels. - final double captureHeight; - /// Build a CameraResolutionChanged event triggered from the camera /// represented by `cameraId`. /// /// The `captureWidth` represents the width of the resulting image in pixels. /// The `captureHeight` represents the height of the resulting image in pixels. - CameraResolutionChangedEvent( + const CameraResolutionChangedEvent( int cameraId, this.captureWidth, this.captureHeight, @@ -146,13 +143,19 @@ class CameraResolutionChangedEvent extends CameraEvent { /// Converts the supplied [Map] to an instance of the /// [CameraResolutionChangedEvent] class. CameraResolutionChangedEvent.fromJson(Map json) - : captureWidth = json['captureWidth'], - captureHeight = json['captureHeight'], - super(json['cameraId']); + : captureWidth = json['captureWidth']! as double, + captureHeight = json['captureHeight']! as double, + super(json['cameraId']! as int); + + /// The capture width in pixels. + final double captureWidth; + + /// The capture height in pixels. + final double captureHeight; /// Converts the [CameraResolutionChangedEvent] instance into a [Map] instance /// that can be serialized to JSON. - Map toJson() => { + Map toJson() => { 'cameraId': cameraId, 'captureWidth': captureWidth, 'captureHeight': captureHeight, @@ -162,64 +165,66 @@ class CameraResolutionChangedEvent extends CameraEvent { bool operator ==(Object other) => identical(this, other) || other is CameraResolutionChangedEvent && - super == (other) && + super == other && runtimeType == other.runtimeType && captureWidth == other.captureWidth && 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. class CameraClosingEvent extends CameraEvent { /// Build a CameraClosing event triggered from the camera represented by /// `cameraId`. - CameraClosingEvent(int cameraId) : super(cameraId); + const CameraClosingEvent(int cameraId) : super(cameraId); /// Converts the supplied [Map] to an instance of the [CameraClosingEvent] /// class. CameraClosingEvent.fromJson(Map json) - : super(json['cameraId']); + : super(json['cameraId']! as int); /// Converts the [CameraClosingEvent] instance into a [Map] instance that can /// be serialized to JSON. - Map toJson() => { + Map toJson() => { 'cameraId': cameraId, }; @override bool operator ==(Object other) => identical(this, other) || - super == (other) && + super == other && other is CameraClosingEvent && runtimeType == other.runtimeType; @override + // This is here even though it just calls super to make it less likely that + // operator== would be changed without changing `hashCode`. + // ignore: unnecessary_overrides int get hashCode => super.hashCode; } /// An event fired when an error occured while operating the camera. class CameraErrorEvent extends CameraEvent { - /// Description of the error. - final String description; - /// Build a CameraError event triggered from the camera represented by /// `cameraId`. /// /// The `description` represents the error occured on the camera. - CameraErrorEvent(int cameraId, this.description) : super(cameraId); + const CameraErrorEvent(int cameraId, this.description) : super(cameraId); /// Converts the supplied [Map] to an instance of the [CameraErrorEvent] /// class. CameraErrorEvent.fromJson(Map json) - : description = json['description'], - super(json['cameraId']); + : description = json['description']! as String, + super(json['cameraId']! as int); + + /// Description of the error. + final String description; /// Converts the [CameraErrorEvent] instance into a [Map] instance that can be /// serialized to JSON. - Map toJson() => { + Map toJson() => { 'cameraId': cameraId, 'description': description, }; @@ -227,43 +232,43 @@ class CameraErrorEvent extends CameraEvent { @override bool operator ==(Object other) => identical(this, other) || - super == (other) && + super == other && other is CameraErrorEvent && runtimeType == other.runtimeType && 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. class VideoRecordedEvent extends CameraEvent { - /// XFile of the recorded video. - final XFile file; - - /// Maximum duration of the recorded video. - final Duration? maxVideoDuration; - /// Build a VideoRecordedEvent triggered from the camera with the `cameraId`. /// /// The `file` represents the file of the video. /// The `maxVideoDuration` shows if a maxVideoDuration shows if a maximum /// video duration was set. - VideoRecordedEvent(int cameraId, this.file, this.maxVideoDuration) + const VideoRecordedEvent(int cameraId, this.file, this.maxVideoDuration) : super(cameraId); /// Converts the supplied [Map] to an instance of the [VideoRecordedEvent] /// class. VideoRecordedEvent.fromJson(Map json) - : file = XFile(json['path']), + : file = XFile(json['path']! as String), maxVideoDuration = json['maxVideoDuration'] != null ? Duration(milliseconds: json['maxVideoDuration'] as int) : null, - super(json['cameraId']); + super(json['cameraId']! as int); + + /// XFile of the recorded video. + final XFile file; + + /// Maximum duration of the recorded video. + final Duration? maxVideoDuration; /// Converts the [VideoRecordedEvent] instance into a [Map] instance that can be /// serialized to JSON. - Map toJson() => { + Map toJson() => { 'cameraId': cameraId, 'path': file.path, 'maxVideoDuration': maxVideoDuration?.inMilliseconds @@ -278,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 ac1c66e4df82..d6bb5df05980 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 @@ -3,6 +3,7 @@ // 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'; /// Generic Event coming from the native side of Camera, @@ -18,24 +19,29 @@ import 'package:flutter/services.dart'; /// See below for examples: `DeviceOrientationChangedEvent`... /// These events are more semantic and more pleasant to use than raw generics. /// They can be (and in fact, are) filtered by the `instanceof`-operator. -abstract class DeviceEvent {} +@immutable +abstract class DeviceEvent { + /// Creates a new device event. + const DeviceEvent(); +} /// The [DeviceOrientationChangedEvent] is fired every time the orientation of the device UI changes. class DeviceOrientationChangedEvent extends DeviceEvent { - /// The new orientation of the device - final DeviceOrientation orientation; - /// Build a new orientation changed event. - DeviceOrientationChangedEvent(this.orientation); + const DeviceOrientationChangedEvent(this.orientation); /// Converts the supplied [Map] to an instance of the [DeviceOrientationChangedEvent] /// class. DeviceOrientationChangedEvent.fromJson(Map json) - : orientation = deserializeDeviceOrientation(json['orientation']); + : orientation = + deserializeDeviceOrientation(json['orientation']! as String); + + /// The new orientation of the device + final DeviceOrientation orientation; /// Converts the [DeviceOrientationChangedEvent] instance into a [Map] instance that /// can be serialized to JSON. - Map toJson() => { + Map toJson() => { 'orientation': serializeDeviceOrientation(orientation), }; 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 f932f253f491..fd9ad530bd27 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 @@ -6,22 +6,27 @@ import 'dart:async'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_platform_interface/src/events/device_event.dart'; -import 'package:camera_platform_interface/src/types/focus_mode.dart'; -import 'package:camera_platform_interface/src/types/image_format_group.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; -import 'package:cross_file/cross_file.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'type_conversion.dart'; + const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); /// An implementation of [CameraPlatform] that uses method channels. class MethodChannelCamera extends CameraPlatform { - final Map _channels = {}; + /// Construct a new method channel camera instance. + MethodChannelCamera() { + const MethodChannel channel = + MethodChannel('flutter.io/cameraPlugin/device'); + channel.setMethodCallHandler( + (MethodCall call) => handleDeviceMethodCall(call)); + } + + final Map _channels = {}; /// The controller we need to broadcast the different events coming /// from handleMethodCall, specific to camera events. @@ -45,16 +50,15 @@ 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((event) => event.cameraId == cameraId); - - /// Construct a new method channel camera instance. - MethodChannelCamera() { - final channel = MethodChannel('flutter.io/cameraPlugin/device'); - channel.setMethodCallHandler( - (MethodCall call) => handleDeviceMethodCall(call)); - } + .where((CameraEvent event) => event.cameraId == cameraId); @override Future> availableCameras() async { @@ -68,9 +72,10 @@ class MethodChannelCamera extends CameraPlatform { return cameras.map((Map camera) { return CameraDescription( - name: camera['name'], - lensDirection: parseCameraLensDirection(camera['lensFacing']), - sensorOrientation: camera['sensorOrientation'], + name: camera['name']! as String, + lensDirection: + parseCameraLensDirection(camera['lensFacing']! as String), + sensorOrientation: camera['sensorOrientation']! as int, ); }).toList(); } on PlatformException catch (e) { @@ -85,7 +90,7 @@ class MethodChannelCamera extends CameraPlatform { bool enableAudio = false, }) async { try { - final reply = await _channel + final Map? reply = await _channel .invokeMapMethod('create', { 'cameraName': cameraDescription.name, 'resolutionPreset': resolutionPreset != null @@ -94,7 +99,7 @@ class MethodChannelCamera extends CameraPlatform { 'enableAudio': enableAudio, }); - return reply!['cameraId']; + return reply!['cameraId']! as int; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -106,15 +111,16 @@ class MethodChannelCamera extends CameraPlatform { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) { _channels.putIfAbsent(cameraId, () { - final channel = MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); + final MethodChannel channel = + MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); channel.setMethodCallHandler( (MethodCall call) => handleCameraMethodCall(call, cameraId)); return channel; }); - Completer _completer = Completer(); + final Completer _completer = Completer(); - onCameraInitialized(cameraId).first.then((value) { + onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) { _completer.complete(); }); @@ -124,6 +130,21 @@ class MethodChannelCamera extends CameraPlatform { 'cameraId': cameraId, 'imageFormatGroup': imageFormatGroup.name(), }, + ) + // 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 + .catchError( + (Object error, StackTrace stackTrace) { + if (error is! PlatformException) { + throw error; + } + _completer.completeError( + CameraException(error.code, error.message), + stackTrace, + ); + }, ); return _completer.future; @@ -132,7 +153,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future dispose(int cameraId) async { if (_channels.containsKey(cameraId)) { - final cameraChannel = _channels[cameraId]; + final MethodChannel? cameraChannel = _channels[cameraId]; cameraChannel?.setMethodCallHandler(null); _channels.remove(cameraId); } @@ -198,7 +219,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future takePicture(int cameraId) async { - final path = await _channel.invokeMethod( + final String? path = await _channel.invokeMethod( 'takePicture', {'cameraId': cameraId}, ); @@ -231,7 +252,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future stopVideoRecording(int cameraId) async { - final path = await _channel.invokeMethod( + final String? path = await _channel.invokeMethod( 'stopVideoRecording', {'cameraId': cameraId}, ); @@ -259,6 +280,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( @@ -297,7 +364,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future getMinExposureOffset(int cameraId) async { - final minExposureOffset = await _channel.invokeMethod( + final double? minExposureOffset = await _channel.invokeMethod( 'getMinExposureOffset', {'cameraId': cameraId}, ); @@ -307,7 +374,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future getMaxExposureOffset(int cameraId) async { - final maxExposureOffset = await _channel.invokeMethod( + final double? maxExposureOffset = await _channel.invokeMethod( 'getMaxExposureOffset', {'cameraId': cameraId}, ); @@ -317,7 +384,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future getExposureOffsetStepSize(int cameraId) async { - final stepSize = await _channel.invokeMethod( + final double? stepSize = await _channel.invokeMethod( 'getExposureOffsetStepSize', {'cameraId': cameraId}, ); @@ -327,7 +394,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future setExposureOffset(int cameraId, double offset) async { - final appliedOffset = await _channel.invokeMethod( + final double? appliedOffset = await _channel.invokeMethod( 'setExposureOffset', { 'cameraId': cameraId, @@ -366,7 +433,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future getMaxZoomLevel(int cameraId) async { - final maxZoomLevel = await _channel.invokeMethod( + final double? maxZoomLevel = await _channel.invokeMethod( 'getMaxZoomLevel', {'cameraId': cameraId}, ); @@ -376,7 +443,7 @@ class MethodChannelCamera extends CameraPlatform { @override Future getMinZoomLevel(int cameraId) async { - final minZoomLevel = await _channel.invokeMethod( + final double? minZoomLevel = await _channel.invokeMethod( 'getMinZoomLevel', {'cameraId': cameraId}, ); @@ -464,7 +531,8 @@ class MethodChannelCamera extends CameraPlatform { switch (call.method) { case 'orientation_changed': deviceEventStreamController.add(DeviceOrientationChangedEvent( - deserializeDeviceOrientation(call.arguments['orientation']))); + deserializeDeviceOrientation( + call.arguments['orientation']! as String))); break; default: throw MissingPluginException(); @@ -481,19 +549,19 @@ class MethodChannelCamera extends CameraPlatform { case 'initialized': cameraEventStreamController.add(CameraInitializedEvent( cameraId, - call.arguments['previewWidth'], - call.arguments['previewHeight'], - deserializeExposureMode(call.arguments['exposureMode']), - call.arguments['exposurePointSupported'], - deserializeFocusMode(call.arguments['focusMode']), - call.arguments['focusPointSupported'], + 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'], - call.arguments['captureHeight'], + call.arguments['captureWidth']! as double, + call.arguments['captureHeight']! as double, )); break; case 'camera_closing': @@ -504,16 +572,17 @@ class MethodChannelCamera extends CameraPlatform { case 'video_recorded': cameraEventStreamController.add(VideoRecordedEvent( cameraId, - XFile(call.arguments['path']), + XFile(call.arguments['path']! as String), call.arguments['maxVideoDuration'] != null - ? Duration(milliseconds: call.arguments['maxVideoDuration']) + ? Duration( + milliseconds: call.arguments['maxVideoDuration']! as int) : null, )); break; case 'error': cameraEventStreamController.add(CameraErrorEvent( cameraId, - call.arguments['description'], + call.arguments['description']! as String, )); break; default: 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 aafeef890f1b..eaa779a943db 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 @@ -6,12 +6,7 @@ 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'; @@ -39,7 +34,7 @@ abstract class CameraPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [CameraPlatform] when they register themselves. static set instance(CameraPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } @@ -154,6 +149,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 98a39fd6c65e..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 @@ -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 'package:flutter/foundation.dart'; + /// The direction the camera is facing. enum CameraLensDirection { /// Front facing camera (a user looking at the screen is seen by the camera). @@ -15,9 +17,10 @@ enum CameraLensDirection { } /// Properties of a camera device. +@immutable class CameraDescription { /// Creates a new camera description with the given properties. - CameraDescription({ + const CameraDescription({ required this.name, required this.lensDirection, required this.sensorOrientation, @@ -47,10 +50,11 @@ class CameraDescription { lensDirection == other.lensDirection; @override - int get hashCode => name.hashCode ^ lensDirection.hashCode; + int get hashCode => Object.hash(name, lensDirection); @override String toString() { - return '$runtimeType($name, $lensDirection, $sensorOrientation)'; + return '${objectRuntimeType(this, 'CameraDescription')}(' + '$name, $lensDirection, $sensorOrientation)'; } } 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/exposure_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart index 1debd19b3a26..56a05cd2d0f1 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart @@ -26,9 +26,9 @@ String serializeExposureMode(ExposureMode exposureMode) { /// Returns the exposure mode for a given String. ExposureMode deserializeExposureMode(String str) { switch (str) { - case "locked": + case 'locked': return ExposureMode.locked; - case "auto": + case 'auto': return ExposureMode.auto; default: throw ArgumentError('"$str" is not a valid ExposureMode value'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart index 60a419155149..6baae0c1f63e 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart @@ -26,9 +26,9 @@ String serializeFocusMode(FocusMode focusMode) { /// Returns the focus mode for a given String. FocusMode deserializeFocusMode(String str) { switch (str) { - case "locked": + case 'locked': return FocusMode.locked; - case "auto": + case 'auto': return FocusMode.auto; default: throw ArgumentError('"$str" is not a valid FocusMode value'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 0927458299df..3eb09fcb833c 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -3,9 +3,10 @@ // found in the LICENSE file. export 'camera_description.dart'; -export 'resolution_preset.dart'; export 'camera_exception.dart'; -export 'flash_mode.dart'; -export 'image_format_group.dart'; +export 'camera_image_data.dart'; export 'exposure_mode.dart'; +export 'flash_mode.dart'; export 'focus_mode.dart'; +export 'image_format_group.dart'; +export 'resolution_preset.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 8c455867762f..663ec6da7a97 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -37,13 +37,13 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { /// Returns the device orientation for a given String. DeviceOrientation deserializeDeviceOrientation(String str) { switch (str) { - case "portraitUp": + case 'portraitUp': return DeviceOrientation.portraitUp; - case "portraitDown": + case 'portraitDown': return DeviceOrientation.portraitDown; - case "landscapeRight": + case 'landscapeRight': return DeviceOrientation.landscapeRight; - case "landscapeLeft": + case 'landscapeLeft': return DeviceOrientation.landscapeLeft; default: throw ArgumentError('"$str" is not a valid DeviceOrientation value'); diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 41c6a9705482..473dcb552c82 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -1,25 +1,23 @@ name: camera_platform_interface description: A common platform interface for the camera plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_platform_interface 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.1 +version: 2.2.0 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: cross_file: ^0.3.1 flutter: sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 stream_transform: ^2.0.0 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 750c27200692..3060089bef40 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 @@ -29,7 +29,7 @@ void main() { 'Default implementation of availableCameras() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -42,7 +42,7 @@ void main() { 'Default implementation of onCameraInitialized() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -55,7 +55,7 @@ void main() { 'Default implementation of onResolutionChanged() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -68,7 +68,7 @@ void main() { 'Default implementation of onCameraClosing() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -81,7 +81,7 @@ void main() { 'Default implementation of onCameraError() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -94,7 +94,7 @@ void main() { 'Default implementation of onDeviceOrientationChanged() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -107,7 +107,7 @@ void main() { 'Default implementation of lockCaptureOrientation() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -121,7 +121,7 @@ void main() { 'Default implementation of unlockCaptureOrientation() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -133,7 +133,7 @@ void main() { test('Default implementation of dispose() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -146,12 +146,12 @@ void main() { 'Default implementation of createCamera() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( () => cameraPlatform.createCamera( - CameraDescription( + const CameraDescription( name: 'back', lensDirection: CameraLensDirection.back, sensorOrientation: 0, @@ -166,7 +166,7 @@ void main() { 'Default implementation of initializeCamera() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -179,7 +179,7 @@ void main() { 'Default implementation of pauseVideoRecording() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -192,7 +192,7 @@ void main() { 'Default implementation of prepareForVideoRecording() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -205,7 +205,7 @@ void main() { 'Default implementation of resumeVideoRecording() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -218,7 +218,7 @@ void main() { 'Default implementation of setFlashMode() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -231,7 +231,7 @@ void main() { 'Default implementation of setExposureMode() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -244,7 +244,7 @@ void main() { 'Default implementation of setExposurePoint() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -257,7 +257,7 @@ void main() { 'Default implementation of getMinExposureOffset() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -270,7 +270,7 @@ void main() { 'Default implementation of getMaxExposureOffset() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -283,7 +283,7 @@ void main() { 'Default implementation of getExposureOffsetStepSize() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -296,7 +296,7 @@ void main() { 'Default implementation of setExposureOffset() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -309,7 +309,7 @@ void main() { 'Default implementation of setFocusMode() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -322,7 +322,7 @@ void main() { 'Default implementation of setFocusPoint() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -335,7 +335,7 @@ void main() { 'Default implementation of startVideoRecording() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -348,7 +348,7 @@ void main() { 'Default implementation of stopVideoRecording() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -361,7 +361,7 @@ void main() { 'Default implementation of takePicture() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -374,7 +374,7 @@ void main() { 'Default implementation of getMaxZoomLevel() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -387,7 +387,7 @@ void main() { 'Default implementation of getMinZoomLevel() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -400,7 +400,7 @@ void main() { 'Default implementation of setZoomLevel() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -413,7 +413,7 @@ void main() { 'Default implementation of pausePreview() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( @@ -426,7 +426,7 @@ void main() { 'Default implementation of resumePreview() should throw unimplemented error', () { // Arrange - final cameraPlatform = ExtendsCameraPlatform(); + final ExtendsCameraPlatform cameraPlatform = ExtendsCameraPlatform(); // Act & Assert expect( 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 637358f557c3..3914859d44b0 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() { @@ -12,7 +10,7 @@ void main() { group('CameraInitializedEvent tests', () { test('Constructor should initialize all properties', () { - final event = CameraInitializedEvent( + const CameraInitializedEvent event = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); expect(event.cameraId, 1); @@ -25,7 +23,8 @@ void main() { }); test('fromJson should initialize all properties', () { - final event = CameraInitializedEvent.fromJson({ + final CameraInitializedEvent event = + CameraInitializedEvent.fromJson(const { 'cameraId': 1, 'previewWidth': 1024.0, 'previewHeight': 640.0, @@ -45,10 +44,10 @@ void main() { }); test('toJson should return a map with all fields', () { - final event = CameraInitializedEvent( + const CameraInitializedEvent event = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final jsonMap = event.toJson(); + final Map jsonMap = event.toJson(); expect(jsonMap.length, 7); expect(jsonMap['cameraId'], 1); @@ -61,45 +60,45 @@ void main() { }); test('equals should return true if objects are the same', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); expect(firstEvent == secondEvent, true); }); test('equals should return false if cameraId is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 2, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if previewWidth is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 2048, 640, ExposureMode.auto, true, FocusMode.auto, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if previewHeight is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 1024, 980, ExposureMode.auto, true, FocusMode.auto, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if exposureMode is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.locked, true, FocusMode.auto, true); expect(firstEvent == secondEvent, false); @@ -107,42 +106,43 @@ void main() { test('equals should return false if exposurePointSupported is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, false, FocusMode.auto, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if focusMode is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.locked, true); expect(firstEvent == secondEvent, false); }); test('equals should return false if focusPointSupported is different', () { - final firstEvent = CameraInitializedEvent( + const CameraInitializedEvent firstEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final secondEvent = CameraInitializedEvent( + const CameraInitializedEvent secondEvent = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, false); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = CameraInitializedEvent( + const CameraInitializedEvent event = CameraInitializedEvent( 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); - final expectedHashCode = event.cameraId.hashCode ^ - event.previewWidth.hashCode ^ - event.previewHeight.hashCode ^ - event.exposureMode.hashCode ^ - event.exposurePointSupported.hashCode ^ - event.focusMode.hashCode ^ - event.focusPointSupported.hashCode; + final int expectedHashCode = Object.hash( + event.cameraId, + event.previewWidth, + event.previewHeight, + event.exposureMode, + event.exposurePointSupported, + event.focusMode, + event.focusPointSupported); expect(event.hashCode, expectedHashCode); }); @@ -150,7 +150,8 @@ void main() { group('CameraResolutionChangesEvent tests', () { test('Constructor should initialize all properties', () { - final event = CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent event = + CameraResolutionChangedEvent(1, 1024, 640); expect(event.cameraId, 1); expect(event.captureWidth, 1024); @@ -158,7 +159,8 @@ void main() { }); test('fromJson should initialize all properties', () { - final event = CameraResolutionChangedEvent.fromJson({ + final CameraResolutionChangedEvent event = + CameraResolutionChangedEvent.fromJson(const { 'cameraId': 1, 'captureWidth': 1024.0, 'captureHeight': 640.0, @@ -170,9 +172,10 @@ void main() { }); test('toJson should return a map with all fields', () { - final event = CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent event = + CameraResolutionChangedEvent(1, 1024, 640); - final jsonMap = event.toJson(); + final Map jsonMap = event.toJson(); expect(jsonMap.length, 3); expect(jsonMap['cameraId'], 1); @@ -181,38 +184,46 @@ void main() { }); test('equals should return true if objects are the same', () { - final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); - final secondEvent = CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent firstEvent = + CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent secondEvent = + CameraResolutionChangedEvent(1, 1024, 640); expect(firstEvent == secondEvent, true); }); test('equals should return false if cameraId is different', () { - final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); - final secondEvent = CameraResolutionChangedEvent(2, 1024, 640); + const CameraResolutionChangedEvent firstEvent = + CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent secondEvent = + CameraResolutionChangedEvent(2, 1024, 640); expect(firstEvent == secondEvent, false); }); test('equals should return false if captureWidth is different', () { - final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); - final secondEvent = CameraResolutionChangedEvent(1, 2048, 640); + const CameraResolutionChangedEvent firstEvent = + CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent secondEvent = + CameraResolutionChangedEvent(1, 2048, 640); expect(firstEvent == secondEvent, false); }); test('equals should return false if captureHeight is different', () { - final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); - final secondEvent = CameraResolutionChangedEvent(1, 1024, 980); + const CameraResolutionChangedEvent firstEvent = + CameraResolutionChangedEvent(1, 1024, 640); + const CameraResolutionChangedEvent secondEvent = + CameraResolutionChangedEvent(1, 1024, 980); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = CameraResolutionChangedEvent(1, 1024, 640); - final expectedHashCode = event.cameraId.hashCode ^ - event.captureWidth.hashCode ^ - event.captureHeight.hashCode; + const CameraResolutionChangedEvent event = + CameraResolutionChangedEvent(1, 1024, 640); + final int expectedHashCode = + Object.hash(event.cameraId, event.captureWidth, event.captureHeight); expect(event.hashCode, expectedHashCode); }); @@ -220,13 +231,14 @@ void main() { group('CameraClosingEvent tests', () { test('Constructor should initialize all properties', () { - final event = CameraClosingEvent(1); + const CameraClosingEvent event = CameraClosingEvent(1); expect(event.cameraId, 1); }); test('fromJson should initialize all properties', () { - final event = CameraClosingEvent.fromJson({ + final CameraClosingEvent event = + CameraClosingEvent.fromJson(const { 'cameraId': 1, }); @@ -234,31 +246,31 @@ void main() { }); test('toJson should return a map with all fields', () { - final event = CameraClosingEvent(1); + const CameraClosingEvent event = CameraClosingEvent(1); - final jsonMap = event.toJson(); + final Map jsonMap = event.toJson(); expect(jsonMap.length, 1); expect(jsonMap['cameraId'], 1); }); test('equals should return true if objects are the same', () { - final firstEvent = CameraClosingEvent(1); - final secondEvent = CameraClosingEvent(1); + const CameraClosingEvent firstEvent = CameraClosingEvent(1); + const CameraClosingEvent secondEvent = CameraClosingEvent(1); expect(firstEvent == secondEvent, true); }); test('equals should return false if cameraId is different', () { - final firstEvent = CameraClosingEvent(1); - final secondEvent = CameraClosingEvent(2); + const CameraClosingEvent firstEvent = CameraClosingEvent(1); + const CameraClosingEvent secondEvent = CameraClosingEvent(2); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = CameraClosingEvent(1); - final expectedHashCode = event.cameraId.hashCode; + const CameraClosingEvent event = CameraClosingEvent(1); + final int expectedHashCode = event.cameraId.hashCode; expect(event.hashCode, expectedHashCode); }); @@ -266,24 +278,24 @@ void main() { group('CameraErrorEvent tests', () { test('Constructor should initialize all properties', () { - final event = CameraErrorEvent(1, 'Error'); + const CameraErrorEvent event = CameraErrorEvent(1, 'Error'); expect(event.cameraId, 1); expect(event.description, 'Error'); }); test('fromJson should initialize all properties', () { - final event = CameraErrorEvent.fromJson( - {'cameraId': 1, 'description': 'Error'}); + final CameraErrorEvent event = CameraErrorEvent.fromJson( + const {'cameraId': 1, 'description': 'Error'}); expect(event.cameraId, 1); expect(event.description, 'Error'); }); test('toJson should return a map with all fields', () { - final event = CameraErrorEvent(1, 'Error'); + const CameraErrorEvent event = CameraErrorEvent(1, 'Error'); - final jsonMap = event.toJson(); + final Map jsonMap = event.toJson(); expect(jsonMap.length, 2); expect(jsonMap['cameraId'], 1); @@ -291,30 +303,30 @@ void main() { }); test('equals should return true if objects are the same', () { - final firstEvent = CameraErrorEvent(1, 'Error'); - final secondEvent = CameraErrorEvent(1, 'Error'); + const CameraErrorEvent firstEvent = CameraErrorEvent(1, 'Error'); + const CameraErrorEvent secondEvent = CameraErrorEvent(1, 'Error'); expect(firstEvent == secondEvent, true); }); test('equals should return false if cameraId is different', () { - final firstEvent = CameraErrorEvent(1, 'Error'); - final secondEvent = CameraErrorEvent(2, 'Error'); + const CameraErrorEvent firstEvent = CameraErrorEvent(1, 'Error'); + const CameraErrorEvent secondEvent = CameraErrorEvent(2, 'Error'); expect(firstEvent == secondEvent, false); }); test('equals should return false if description is different', () { - final firstEvent = CameraErrorEvent(1, 'Error'); - final secondEvent = CameraErrorEvent(1, 'Ooops'); + const CameraErrorEvent firstEvent = CameraErrorEvent(1, 'Error'); + const CameraErrorEvent secondEvent = CameraErrorEvent(1, 'Ooops'); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = CameraErrorEvent(1, 'Error'); - final expectedHashCode = - event.cameraId.hashCode ^ event.description.hashCode; + const CameraErrorEvent event = CameraErrorEvent(1, 'Error'); + final int expectedHashCode = + Object.hash(event.cameraId, event.description); expect(event.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart index f7cb657725a9..11f786c0df4c 100644 --- a/packages/camera/camera_platform_interface/test/events/device_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.dart @@ -11,13 +11,15 @@ void main() { group('DeviceOrientationChangedEvent tests', () { test('Constructor should initialize all properties', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + const DeviceOrientationChangedEvent event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); expect(event.orientation, DeviceOrientation.portraitUp); }); test('fromJson should initialize all properties', () { - final event = DeviceOrientationChangedEvent.fromJson({ + final DeviceOrientationChangedEvent event = + DeviceOrientationChangedEvent.fromJson(const { 'orientation': 'portraitUp', }); @@ -25,35 +27,37 @@ void main() { }); test('toJson should return a map with all fields', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + const DeviceOrientationChangedEvent event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); - final jsonMap = event.toJson(); + final Map jsonMap = event.toJson(); expect(jsonMap.length, 1); expect(jsonMap['orientation'], 'portraitUp'); }); test('equals should return true if objects are the same', () { - final firstEvent = + const DeviceOrientationChangedEvent firstEvent = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); - final secondEvent = + const DeviceOrientationChangedEvent secondEvent = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); expect(firstEvent == secondEvent, true); }); test('equals should return false if orientation is different', () { - final firstEvent = + const DeviceOrientationChangedEvent firstEvent = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); - final secondEvent = + const DeviceOrientationChangedEvent secondEvent = DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { - final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); - final expectedHashCode = event.orientation.hashCode; + const DeviceOrientationChangedEvent event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final int expectedHashCode = event.orientation.hashCode; expect(event.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index ec71aa173fff..d096f0012c86 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'; @@ -25,19 +22,19 @@ void main() { group('Creation, Initialization & Disposal Tests', () { test('Should send creation data and receive back a camera id', () async { // Arrange - MethodChannelMock cameraMockChannel = MethodChannelMock( + final MethodChannelMock cameraMockChannel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: { - 'create': { + methods: { + 'create': { 'cameraId': 1, 'imageFormatGroup': 'unknown', } }); - final camera = MethodChannelCamera(); + final MethodChannelCamera camera = MethodChannelCamera(); // Act - final cameraId = await camera.createCamera( - CameraDescription( + final int cameraId = await camera.createCamera( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0), @@ -48,7 +45,7 @@ void main() { expect(cameraMockChannel.log, [ isMethodCall( 'create', - arguments: { + arguments: { 'cameraName': 'Test', 'resolutionPreset': 'high', 'enableAudio': false @@ -62,18 +59,20 @@ void main() { 'Should throw CameraException when create throws a PlatformException', () { // Arrange - MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { - 'create': PlatformException( - code: 'TESTING_ERROR_CODE', - message: 'Mock error message used during testing.', - ) - }); - final camera = MethodChannelCamera(); + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final MethodChannelCamera camera = MethodChannelCamera(); // Act expect( () => camera.createCamera( - CameraDescription( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, @@ -82,8 +81,9 @@ void main() { ), throwsA( isA() - .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') - .having((e) => e.description, 'description', + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', 'Mock error message used during testing.'), ), ); @@ -93,18 +93,20 @@ void main() { 'Should throw CameraException when create throws a PlatformException', () { // Arrange - MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { - 'create': PlatformException( - code: 'TESTING_ERROR_CODE', - message: 'Mock error message used during testing.', - ) - }); - final camera = MethodChannelCamera(); + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final MethodChannelCamera camera = MethodChannelCamera(); // Act expect( () => camera.createCamera( - CameraDescription( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, @@ -113,27 +115,60 @@ void main() { ), throwsA( isA() - .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') - .having((e) => e.description, 'description', + .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: 'plugins.flutter.io/camera', + methods: { + 'initialize': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }, + ); + final MethodChannelCamera camera = MethodChannelCamera(); + + // 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 - MethodChannelMock cameraMockChannel = MethodChannelMock( + final MethodChannelMock cameraMockChannel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: { - 'create': { + methods: { + 'create': { 'cameraId': 1, 'imageFormatGroup': 'unknown', }, 'initialize': null }); - final camera = MethodChannelCamera(); - final cameraId = await camera.createCamera( - CameraDescription( + final MethodChannelCamera camera = MethodChannelCamera(); + final int cameraId = await camera.createCamera( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, @@ -142,7 +177,7 @@ void main() { ); // Act - Future initializeFuture = camera.initializeCamera(cameraId); + final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add(CameraInitializedEvent( cameraId, 1920, @@ -160,7 +195,7 @@ void main() { anything, isMethodCall( 'initialize', - arguments: { + arguments: { 'cameraId': 1, 'imageFormatGroup': 'unknown', }, @@ -170,24 +205,24 @@ void main() { test('Should send a disposal call on dispose', () async { // Arrange - MethodChannelMock cameraMockChannel = MethodChannelMock( + final MethodChannelMock cameraMockChannel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: { - 'create': {'cameraId': 1}, + methods: { + 'create': {'cameraId': 1}, 'initialize': null, - 'dispose': {'cameraId': 1} + 'dispose': {'cameraId': 1} }); - final camera = MethodChannelCamera(); - final cameraId = await camera.createCamera( - CameraDescription( + final MethodChannelCamera camera = MethodChannelCamera(); + final int cameraId = await camera.createCamera( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); - Future initializeFuture = camera.initializeCamera(cameraId); + final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add(CameraInitializedEvent( cameraId, 1920, @@ -209,7 +244,7 @@ void main() { anything, isMethodCall( 'dispose', - arguments: {'cameraId': 1}, + arguments: {'cameraId': 1}, ), ]); }); @@ -221,21 +256,21 @@ void main() { setUp(() async { MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: { - 'create': {'cameraId': 1}, + methods: { + 'create': {'cameraId': 1}, 'initialize': null }, ); camera = MethodChannelCamera(); cameraId = await camera.createCamera( - CameraDescription( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); - Future initializeFuture = camera.initializeCamera(cameraId); + final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add(CameraInitializedEvent( cameraId, 1920, @@ -252,10 +287,11 @@ void main() { // Act final Stream eventStream = camera.onCameraInitialized(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); // Emit test events - final event = CameraInitializedEvent( + final CameraInitializedEvent event = CameraInitializedEvent( cameraId, 3840, 2160, @@ -278,11 +314,14 @@ void main() { // Act final Stream resolutionStream = camera.onCameraResolutionChanged(cameraId); - final streamQueue = StreamQueue(resolutionStream); + final StreamQueue streamQueue = + StreamQueue(resolutionStream); // Emit test events - final fhdEvent = CameraResolutionChangedEvent(cameraId, 1920, 1080); - final uhdEvent = CameraResolutionChangedEvent(cameraId, 3840, 2160); + 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( @@ -306,10 +345,11 @@ void main() { // Act final Stream eventStream = camera.onCameraClosing(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); // Emit test events - final event = CameraClosingEvent(cameraId); + final CameraClosingEvent event = CameraClosingEvent(cameraId); await camera.handleCameraMethodCall( MethodCall('camera_closing', event.toJson()), cameraId); await camera.handleCameraMethodCall( @@ -328,11 +368,14 @@ void main() { test('Should receive camera error events', () async { // Act - final errorStream = camera.onCameraError(cameraId); - final streamQueue = StreamQueue(errorStream); + final Stream errorStream = + camera.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(errorStream); // Emit test events - final event = CameraErrorEvent(cameraId, 'Error Description'); + final CameraErrorEvent event = + CameraErrorEvent(cameraId, 'Error Description'); await camera.handleCameraMethodCall( MethodCall('error', event.toJson()), cameraId); await camera.handleCameraMethodCall( @@ -351,11 +394,13 @@ void main() { test('Should receive device orientation change events', () async { // Act - final eventStream = camera.onDeviceOrientationChanged(); - final streamQueue = StreamQueue(eventStream); + final Stream eventStream = + camera.onDeviceOrientationChanged(); + final StreamQueue streamQueue = + StreamQueue(eventStream); // Emit test events - final event = + const DeviceOrientationChangedEvent event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); await camera.handleDeviceMethodCall( MethodCall('orientation_changed', event.toJson())); @@ -381,21 +426,21 @@ void main() { setUp(() async { MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: { - 'create': {'cameraId': 1}, + methods: { + 'create': {'cameraId': 1}, 'initialize': null }, ); camera = MethodChannelCamera(); cameraId = await camera.createCamera( - CameraDescription( + const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); - Future initializeFuture = camera.initializeCamera(cameraId); + final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add( CameraInitializedEvent( cameraId, @@ -413,17 +458,25 @@ void main() { test('Should fetch CameraDescription instances for available cameras', () async { // Arrange - List> returnData = [ - {'name': 'Test 1', 'lensFacing': 'front', 'sensorOrientation': 1}, - {'name': 'Test 2', 'lensFacing': 'back', 'sensorOrientation': 2} + final List returnData = [ + { + 'name': 'Test 1', + 'lensFacing': 'front', + 'sensorOrientation': 1 + }, + { + 'name': 'Test 2', + 'lensFacing': 'back', + 'sensorOrientation': 2 + } ]; - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'availableCameras': returnData}, + methods: {'availableCameras': returnData}, ); // Act - List cameras = await camera.availableCameras(); + final List cameras = await camera.availableCameras(); // Assert expect(channel.log, [ @@ -431,11 +484,11 @@ void main() { ]); expect(cameras.length, returnData.length); for (int i = 0; i < returnData.length; i++) { - CameraDescription cameraDescription = CameraDescription( - name: returnData[i]['name'], - lensDirection: - parseCameraLensDirection(returnData[i]['lensFacing']), - sensorOrientation: returnData[i]['sensorOrientation'], + 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); } @@ -445,20 +498,23 @@ void main() { 'Should throw CameraException when availableCameras throws a PlatformException', () { // Arrange - MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { - 'availableCameras': PlatformException( - code: 'TESTING_ERROR_CODE', - message: 'Mock error message used during testing.', - ) - }); + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); // Act expect( camera.availableCameras, throwsA( isA() - .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') - .having((e) => e.description, 'description', + .having( + (CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((CameraException e) => e.description, 'description', 'Mock error message used during testing.'), ), ); @@ -466,16 +522,16 @@ void main() { test('Should take a picture and return an XFile instance', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'takePicture': '/test/path.jpg'}); + methods: {'takePicture': '/test/path.jpg'}); // Act - XFile file = await camera.takePicture(cameraId); + final XFile file = await camera.takePicture(cameraId); // Assert expect(channel.log, [ - isMethodCall('takePicture', arguments: { + isMethodCall('takePicture', arguments: { 'cameraId': cameraId, }), ]); @@ -484,9 +540,9 @@ void main() { test('Should prepare for video recording', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'prepareForVideoRecording': null}, + methods: {'prepareForVideoRecording': null}, ); // Act @@ -500,9 +556,9 @@ void main() { test('Should start recording a video', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'startVideoRecording': null}, + methods: {'startVideoRecording': null}, ); // Act @@ -510,7 +566,7 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('startVideoRecording', arguments: { + isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, }), @@ -520,37 +576,39 @@ void main() { test('Should pass maxVideoDuration when starting recording a video', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'startVideoRecording': null}, + methods: {'startVideoRecording': null}, ); // Act await camera.startVideoRecording( cameraId, - maxVideoDuration: Duration(seconds: 10), + maxVideoDuration: const Duration(seconds: 10), ); // Assert expect(channel.log, [ - isMethodCall('startVideoRecording', - arguments: {'cameraId': cameraId, 'maxVideoDuration': 10000}), + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': 10000 + }), ]); }); test('Should stop a video recording and return the file', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'stopVideoRecording': '/test/path.mp4'}, + methods: {'stopVideoRecording': '/test/path.mp4'}, ); // Act - XFile file = await camera.stopVideoRecording(cameraId); + final XFile file = await camera.stopVideoRecording(cameraId); // Assert expect(channel.log, [ - isMethodCall('stopVideoRecording', arguments: { + isMethodCall('stopVideoRecording', arguments: { 'cameraId': cameraId, }), ]); @@ -559,9 +617,9 @@ void main() { test('Should pause a video recording', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'pauseVideoRecording': null}, + methods: {'pauseVideoRecording': null}, ); // Act @@ -569,7 +627,7 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('pauseVideoRecording', arguments: { + isMethodCall('pauseVideoRecording', arguments: { 'cameraId': cameraId, }), ]); @@ -577,9 +635,9 @@ void main() { test('Should resume a video recording', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'resumeVideoRecording': null}, + methods: {'resumeVideoRecording': null}, ); // Act @@ -587,7 +645,7 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('resumeVideoRecording', arguments: { + isMethodCall('resumeVideoRecording', arguments: { 'cameraId': cameraId, }), ]); @@ -595,9 +653,9 @@ void main() { test('Should set the flash mode', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setFlashMode': null}, + methods: {'setFlashMode': null}, ); // Act @@ -608,22 +666,30 @@ void main() { // 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'}), + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'torch' + }), + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'always' + }), + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'auto' + }), + isMethodCall('setFlashMode', arguments: { + 'cameraId': cameraId, + 'mode': 'off' + }), ]); }); test('Should set the exposure mode', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setExposureMode': null}, + methods: {'setExposureMode': null}, ); // Act @@ -632,33 +698,37 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('setExposureMode', - arguments: {'cameraId': cameraId, 'mode': 'auto'}), - isMethodCall('setExposureMode', - arguments: {'cameraId': cameraId, 'mode': 'locked'}), + isMethodCall('setExposureMode', arguments: { + 'cameraId': cameraId, + 'mode': 'auto' + }), + isMethodCall('setExposureMode', arguments: { + 'cameraId': cameraId, + 'mode': 'locked' + }), ]); }); test('Should set the exposure point', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setExposurePoint': null}, + methods: {'setExposurePoint': null}, ); // Act - await camera.setExposurePoint(cameraId, Point(0.5, 0.5)); + await camera.setExposurePoint(cameraId, const Point(0.5, 0.5)); await camera.setExposurePoint(cameraId, null); // Assert expect(channel.log, [ - isMethodCall('setExposurePoint', arguments: { + isMethodCall('setExposurePoint', arguments: { 'cameraId': cameraId, 'x': 0.5, 'y': 0.5, 'reset': false }), - isMethodCall('setExposurePoint', arguments: { + isMethodCall('setExposurePoint', arguments: { 'cameraId': cameraId, 'x': null, 'y': null, @@ -669,18 +739,19 @@ void main() { test('Should get the min exposure offset', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'getMinExposureOffset': 2.0}, + methods: {'getMinExposureOffset': 2.0}, ); // Act - final minExposureOffset = await camera.getMinExposureOffset(cameraId); + final double minExposureOffset = + await camera.getMinExposureOffset(cameraId); // Assert expect(minExposureOffset, 2.0); expect(channel.log, [ - isMethodCall('getMinExposureOffset', arguments: { + isMethodCall('getMinExposureOffset', arguments: { 'cameraId': cameraId, }), ]); @@ -688,18 +759,19 @@ void main() { test('Should get the max exposure offset', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'getMaxExposureOffset': 2.0}, + methods: {'getMaxExposureOffset': 2.0}, ); // Act - final maxExposureOffset = await camera.getMaxExposureOffset(cameraId); + final double maxExposureOffset = + await camera.getMaxExposureOffset(cameraId); // Assert expect(maxExposureOffset, 2.0); expect(channel.log, [ - isMethodCall('getMaxExposureOffset', arguments: { + isMethodCall('getMaxExposureOffset', arguments: { 'cameraId': cameraId, }), ]); @@ -707,37 +779,40 @@ void main() { test('Should get the exposure offset step size', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'getExposureOffsetStepSize': 0.25}, + methods: {'getExposureOffsetStepSize': 0.25}, ); // Act - final stepSize = await camera.getExposureOffsetStepSize(cameraId); + final double stepSize = + await camera.getExposureOffsetStepSize(cameraId); // Assert expect(stepSize, 0.25); expect(channel.log, [ - isMethodCall('getExposureOffsetStepSize', arguments: { - 'cameraId': cameraId, - }), + isMethodCall('getExposureOffsetStepSize', + arguments: { + 'cameraId': cameraId, + }), ]); }); test('Should set the exposure offset', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setExposureOffset': 0.6}, + methods: {'setExposureOffset': 0.6}, ); // Act - final actualOffset = await camera.setExposureOffset(cameraId, 0.5); + final double actualOffset = + await camera.setExposureOffset(cameraId, 0.5); // Assert expect(actualOffset, 0.6); expect(channel.log, [ - isMethodCall('setExposureOffset', arguments: { + isMethodCall('setExposureOffset', arguments: { 'cameraId': cameraId, 'offset': 0.5, }), @@ -746,9 +821,9 @@ void main() { test('Should set the focus mode', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setFocusMode': null}, + methods: {'setFocusMode': null}, ); // Act @@ -757,33 +832,37 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('setFocusMode', - arguments: {'cameraId': cameraId, 'mode': 'auto'}), - isMethodCall('setFocusMode', - arguments: {'cameraId': cameraId, 'mode': 'locked'}), + isMethodCall('setFocusMode', arguments: { + 'cameraId': cameraId, + 'mode': 'auto' + }), + isMethodCall('setFocusMode', arguments: { + 'cameraId': cameraId, + 'mode': 'locked' + }), ]); }); test('Should set the exposure point', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setFocusPoint': null}, + methods: {'setFocusPoint': null}, ); // Act - await camera.setFocusPoint(cameraId, Point(0.5, 0.5)); + await camera.setFocusPoint(cameraId, const Point(0.5, 0.5)); await camera.setFocusPoint(cameraId, null); // Assert expect(channel.log, [ - isMethodCall('setFocusPoint', arguments: { + isMethodCall('setFocusPoint', arguments: { 'cameraId': cameraId, 'x': 0.5, 'y': 0.5, 'reset': false }), - isMethodCall('setFocusPoint', arguments: { + isMethodCall('setFocusPoint', arguments: { 'cameraId': cameraId, 'x': null, 'y': null, @@ -794,7 +873,7 @@ void main() { test('Should build a texture widget as preview widget', () async { // Act - Widget widget = camera.buildPreview(cameraId); + final Widget widget = camera.buildPreview(cameraId); // Act expect(widget is Texture, isTrue); @@ -803,28 +882,28 @@ void main() { test('Should throw MissingPluginException when handling unknown method', () { - final camera = MethodChannelCamera(); + final MethodChannelCamera camera = MethodChannelCamera(); expect( - () => - camera.handleCameraMethodCall(MethodCall('unknown_method'), 1), + () => camera.handleCameraMethodCall( + const MethodCall('unknown_method'), 1), throwsA(isA())); }); test('Should get the max zoom level', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'getMaxZoomLevel': 10.0}, + methods: {'getMaxZoomLevel': 10.0}, ); // Act - final maxZoomLevel = await camera.getMaxZoomLevel(cameraId); + final double maxZoomLevel = await camera.getMaxZoomLevel(cameraId); // Assert expect(maxZoomLevel, 10.0); expect(channel.log, [ - isMethodCall('getMaxZoomLevel', arguments: { + isMethodCall('getMaxZoomLevel', arguments: { 'cameraId': cameraId, }), ]); @@ -832,18 +911,18 @@ void main() { test('Should get the min zoom level', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'getMinZoomLevel': 1.0}, + methods: {'getMinZoomLevel': 1.0}, ); // Act - final maxZoomLevel = await camera.getMinZoomLevel(cameraId); + final double maxZoomLevel = await camera.getMinZoomLevel(cameraId); // Assert expect(maxZoomLevel, 1.0); expect(channel.log, [ - isMethodCall('getMinZoomLevel', arguments: { + isMethodCall('getMinZoomLevel', arguments: { 'cameraId': cameraId, }), ]); @@ -851,9 +930,9 @@ void main() { test('Should set the zoom level', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'setZoomLevel': null}, + methods: {'setZoomLevel': null}, ); // Act @@ -862,7 +941,7 @@ void main() { // Assert expect(channel.log, [ isMethodCall('setZoomLevel', - arguments: {'cameraId': cameraId, 'zoom': 2.0}), + arguments: {'cameraId': cameraId, 'zoom': 2.0}), ]); }); @@ -871,7 +950,7 @@ void main() { // Arrange MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: { + methods: { 'setZoomLevel': PlatformException( code: 'ZOOM_ERROR', message: 'Illegal zoom error', @@ -884,16 +963,16 @@ void main() { expect( () => camera.setZoomLevel(cameraId, -1.0), throwsA(isA() - .having((e) => e.code, 'code', 'ZOOM_ERROR') - .having((e) => e.description, 'description', + .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 - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'lockCaptureOrientation': null}, + methods: {'lockCaptureOrientation': null}, ); // Act @@ -902,16 +981,18 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('lockCaptureOrientation', - arguments: {'cameraId': cameraId, 'orientation': 'portraitUp'}), + isMethodCall('lockCaptureOrientation', arguments: { + 'cameraId': cameraId, + 'orientation': 'portraitUp' + }), ]); }); test('Should unlock the capture orientation', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'unlockCaptureOrientation': null}, + methods: {'unlockCaptureOrientation': null}, ); // Act @@ -920,15 +1001,15 @@ void main() { // Assert expect(channel.log, [ isMethodCall('unlockCaptureOrientation', - arguments: {'cameraId': cameraId}), + arguments: {'cameraId': cameraId}), ]); }); test('Should pause the camera preview', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'pausePreview': null}, + methods: {'pausePreview': null}, ); // Act @@ -936,15 +1017,16 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('pausePreview', arguments: {'cameraId': cameraId}), + isMethodCall('pausePreview', + arguments: {'cameraId': cameraId}), ]); }); test('Should resume the camera preview', () async { // Arrange - MethodChannelMock channel = MethodChannelMock( + final MethodChannelMock channel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', - methods: {'resumePreview': null}, + methods: {'resumePreview': null}, ); // Act @@ -952,7 +1034,54 @@ void main() { // Assert expect(channel.log, [ - isMethodCall('resumePreview', arguments: {'cameraId': cameraId}), + isMethodCall('resumePreview', + 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 11a5210e831b..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 @@ -10,13 +10,13 @@ void main() { group('CameraLensDirection tests', () { test('CameraLensDirection should contain 3 options', () { - final values = CameraLensDirection.values; + const List values = CameraLensDirection.values; expect(values.length, 3); }); - test("CameraLensDirection enum should have items in correct index", () { - final values = CameraLensDirection.values; + test('CameraLensDirection enum should have items in correct index', () { + const List values = CameraLensDirection.values; expect(values[0], CameraLensDirection.front); expect(values[1], CameraLensDirection.back); @@ -26,7 +26,7 @@ void main() { group('CameraDescription tests', () { test('Constructor should initialize all properties', () { - final description = CameraDescription( + const CameraDescription description = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 90, @@ -38,12 +38,12 @@ void main() { }); test('equals should return true if objects are the same', () { - final firstDescription = CameraDescription( + const CameraDescription firstDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 90, ); - final secondDescription = CameraDescription( + const CameraDescription secondDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 90, @@ -53,12 +53,12 @@ void main() { }); test('equals should return false if name is different', () { - final firstDescription = CameraDescription( + const CameraDescription firstDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 90, ); - final secondDescription = CameraDescription( + const CameraDescription secondDescription = CameraDescription( name: 'Testing', lensDirection: CameraLensDirection.front, sensorOrientation: 90, @@ -68,12 +68,12 @@ void main() { }); test('equals should return false if lens direction is different', () { - final firstDescription = CameraDescription( + const CameraDescription firstDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 90, ); - final secondDescription = CameraDescription( + const CameraDescription secondDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 90, @@ -83,12 +83,12 @@ void main() { }); test('equals should return true if sensor orientation is different', () { - final firstDescription = CameraDescription( + const CameraDescription firstDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 0, ); - final secondDescription = CameraDescription( + const CameraDescription secondDescription = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 90, @@ -97,15 +97,15 @@ void main() { expect(firstDescription == secondDescription, true); }); - test('hashCode should match hashCode of all properties', () { - final description = CameraDescription( + test('hashCode should match hashCode of all equality-tested properties', + () { + const CameraDescription description = CameraDescription( name: 'Test', lensDirection: CameraLensDirection.front, sensorOrientation: 0, ); - final 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_exception_test.dart b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart index 5fb753fb3616..27baa9cdbe51 100644 --- a/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart +++ b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart @@ -7,21 +7,21 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('constructor should initialize properties', () { - final code = 'TEST_ERROR'; - final description = 'This is a test error'; - final exception = CameraException(code, description); + const String code = 'TEST_ERROR'; + const String description = 'This is a test error'; + final CameraException exception = CameraException(code, description); expect(exception.code, code); expect(exception.description, description); }); test('toString: Should return a description of the exception', () { - final code = 'TEST_ERROR'; - final description = 'This is a test error'; - final expected = 'CameraException($code, $description)'; - final exception = CameraException(code, description); + const String code = 'TEST_ERROR'; + const String description = 'This is a test error'; + const String expected = 'CameraException($code, $description)'; + final CameraException exception = CameraException(code, description); - final actual = exception.toString(); + final String actual = exception.toString(); expect(actual, expected); }); 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_platform_interface/test/types/exposure_mode_test.dart b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart index 659b75e017f1..7dd382450228 100644 --- a/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart +++ b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart @@ -8,24 +8,24 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('ExposureMode should contain 2 options', () { - final values = ExposureMode.values; + const List values = ExposureMode.values; expect(values.length, 2); }); - test("ExposureMode enum should have items in correct index", () { - final values = ExposureMode.values; + test('ExposureMode enum should have items in correct index', () { + const List values = ExposureMode.values; expect(values[0], ExposureMode.auto); expect(values[1], ExposureMode.locked); }); - test("serializeExposureMode() should serialize correctly", () { - expect(serializeExposureMode(ExposureMode.auto), "auto"); - expect(serializeExposureMode(ExposureMode.locked), "locked"); + test('serializeExposureMode() should serialize correctly', () { + expect(serializeExposureMode(ExposureMode.auto), 'auto'); + expect(serializeExposureMode(ExposureMode.locked), 'locked'); }); - test("deserializeExposureMode() should deserialize correctly", () { + test('deserializeExposureMode() should deserialize correctly', () { expect(deserializeExposureMode('auto'), ExposureMode.auto); expect(deserializeExposureMode('locked'), ExposureMode.locked); }); diff --git a/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart b/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart index d313bcf52b77..bfc38a09580a 100644 --- a/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart +++ b/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart @@ -7,13 +7,13 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('FlashMode should contain 4 options', () { - final values = FlashMode.values; + const List values = FlashMode.values; expect(values.length, 4); }); - test("FlashMode enum should have items in correct index", () { - final values = FlashMode.values; + test('FlashMode enum should have items in correct index', () { + const List values = FlashMode.values; expect(values[0], FlashMode.off); expect(values[1], FlashMode.auto); diff --git a/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart b/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart index 866f8ce07909..b7e5abfdee8e 100644 --- a/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart +++ b/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart @@ -7,24 +7,24 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('FocusMode should contain 2 options', () { - final values = FocusMode.values; + const List values = FocusMode.values; expect(values.length, 2); }); - test("FocusMode enum should have items in correct index", () { - final values = FocusMode.values; + test('FocusMode enum should have items in correct index', () { + const List values = FocusMode.values; expect(values[0], FocusMode.auto); expect(values[1], FocusMode.locked); }); - test("serializeFocusMode() should serialize correctly", () { - expect(serializeFocusMode(FocusMode.auto), "auto"); - expect(serializeFocusMode(FocusMode.locked), "locked"); + test('serializeFocusMode() should serialize correctly', () { + expect(serializeFocusMode(FocusMode.auto), 'auto'); + expect(serializeFocusMode(FocusMode.locked), 'locked'); }); - test("deserializeFocusMode() should deserialize correctly", () { + test('deserializeFocusMode() should deserialize correctly', () { expect(deserializeFocusMode('auto'), FocusMode.auto); expect(deserializeFocusMode('locked'), FocusMode.locked); }); diff --git a/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart index 55a4ac56cd9d..abc339729462 100644 --- a/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart +++ b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart @@ -7,13 +7,13 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('ResolutionPreset should contain 6 options', () { - final values = ResolutionPreset.values; + const List values = ResolutionPreset.values; expect(values.length, 6); }); - test("ResolutionPreset enum should have items in correct index", () { - final values = ResolutionPreset.values; + test('ResolutionPreset enum should have items in correct index', () { + const List values = ResolutionPreset.values; expect(values[0], ResolutionPreset.low); expect(values[1], ResolutionPreset.medium); diff --git a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart index 60d8def6a2e3..413c10633cc1 100644 --- a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart +++ b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart @@ -6,11 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; class MethodChannelMock { - final Duration? delay; - final MethodChannel methodChannel; - final Map methods; - final log = []; - MethodChannelMock({ required String channelName, this.delay, @@ -19,7 +14,12 @@ class MethodChannelMock { methodChannel.setMockMethodCallHandler(_handler); } - Future _handler(MethodCall methodCall) async { + 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)) { @@ -27,13 +27,13 @@ class MethodChannelMock { '${methodCall.method} on channel ${methodChannel.name}'); } - return Future.delayed(delay ?? Duration.zero, () { - final result = methods[methodCall.method]; + return Future.delayed(delay ?? Duration.zero, () { + final dynamic result = methods[methodCall.method]; if (result is Exception) { throw result; } - return Future.value(result); + return Future.value(result); }); } } diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart index f1960eeb2e72..0e4171d73aa6 100644 --- a/packages/camera/camera_platform_interface/test/utils/utils_test.dart +++ b/packages/camera/camera_platform_interface/test/utils/utils_test.dart @@ -35,18 +35,18 @@ void main() { ); }); - test("serializeDeviceOrientation() should serialize correctly", () { + test('serializeDeviceOrientation() should serialize correctly', () { expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), - "portraitUp"); + 'portraitUp'); expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), - "portraitDown"); + 'portraitDown'); expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), - "landscapeRight"); + 'landscapeRight'); expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), - "landscapeLeft"); + 'landscapeLeft'); }); - test("deserializeDeviceOrientation() should deserialize correctly", () { + test('deserializeDeviceOrientation() should deserialize correctly', () { expect(deserializeDeviceOrientation('portraitUp'), DeviceOrientation.portraitUp); expect(deserializeDeviceOrientation('portraitDown'), diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 8596b3595852..a3522cfee52d 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,38 @@ +## NEXT + +* 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. + +## 0.2.1+2 + +* Fixes cameraNotReadable error that prevented access to the camera on some Android devices when initializing a camera. +* Implemented support for new Dart SDKs with an async requestFullscreen API. + +## 0.2.1+1 + +* Update usage documentation. + ## 0.2.1 * Add video recording functionality. diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index 918e695496a4..04bf665c1039 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -8,8 +8,9 @@ The web implementation of [`camera`][camera]. ### Depend on the package -This package is not an [endorsed implementation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin) -of the camera plugin yet, so you'll need to [add it explicitly](https://pub.dev/packages/camera_web/install). +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `camera` +normally. This package will be automatically included in your app when you do. ## Example 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 a298b57dfd7f..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 @@ -15,112 +15,112 @@ void main() { group('CameraErrorCode', () { group('toString returns a correct type for', () { - testWidgets('notSupported', (tester) async { + testWidgets('notSupported', (WidgetTester tester) async { expect( CameraErrorCode.notSupported.toString(), equals('cameraNotSupported'), ); }); - testWidgets('notFound', (tester) async { + testWidgets('notFound', (WidgetTester tester) async { expect( CameraErrorCode.notFound.toString(), equals('cameraNotFound'), ); }); - testWidgets('notReadable', (tester) async { + testWidgets('notReadable', (WidgetTester tester) async { expect( CameraErrorCode.notReadable.toString(), equals('cameraNotReadable'), ); }); - testWidgets('overconstrained', (tester) async { + testWidgets('overconstrained', (WidgetTester tester) async { expect( CameraErrorCode.overconstrained.toString(), equals('cameraOverconstrained'), ); }); - testWidgets('permissionDenied', (tester) async { + testWidgets('permissionDenied', (WidgetTester tester) async { expect( CameraErrorCode.permissionDenied.toString(), - equals('cameraPermission'), + equals('CameraAccessDenied'), ); }); - testWidgets('type', (tester) async { + testWidgets('type', (WidgetTester tester) async { expect( CameraErrorCode.type.toString(), equals('cameraType'), ); }); - testWidgets('abort', (tester) async { + testWidgets('abort', (WidgetTester tester) async { expect( CameraErrorCode.abort.toString(), equals('cameraAbort'), ); }); - testWidgets('security', (tester) async { + testWidgets('security', (WidgetTester tester) async { expect( CameraErrorCode.security.toString(), equals('cameraSecurity'), ); }); - testWidgets('missingMetadata', (tester) async { + testWidgets('missingMetadata', (WidgetTester tester) async { expect( CameraErrorCode.missingMetadata.toString(), equals('cameraMissingMetadata'), ); }); - testWidgets('orientationNotSupported', (tester) async { + testWidgets('orientationNotSupported', (WidgetTester tester) async { expect( CameraErrorCode.orientationNotSupported.toString(), equals('orientationNotSupported'), ); }); - testWidgets('torchModeNotSupported', (tester) async { + testWidgets('torchModeNotSupported', (WidgetTester tester) async { expect( CameraErrorCode.torchModeNotSupported.toString(), equals('torchModeNotSupported'), ); }); - testWidgets('zoomLevelNotSupported', (tester) async { + testWidgets('zoomLevelNotSupported', (WidgetTester tester) async { expect( CameraErrorCode.zoomLevelNotSupported.toString(), equals('zoomLevelNotSupported'), ); }); - testWidgets('zoomLevelInvalid', (tester) async { + testWidgets('zoomLevelInvalid', (WidgetTester tester) async { expect( CameraErrorCode.zoomLevelInvalid.toString(), equals('zoomLevelInvalid'), ); }); - testWidgets('notStarted', (tester) async { + testWidgets('notStarted', (WidgetTester tester) async { expect( CameraErrorCode.notStarted.toString(), equals('cameraNotStarted'), ); }); - testWidgets('videoRecordingNotStarted', (tester) async { + testWidgets('videoRecordingNotStarted', (WidgetTester tester) async { expect( CameraErrorCode.videoRecordingNotStarted.toString(), equals('videoRecordingNotStarted'), ); }); - testWidgets('unknown', (tester) async { + testWidgets('unknown', (WidgetTester tester) async { expect( CameraErrorCode.unknown.toString(), equals('cameraUnknown'), @@ -128,7 +128,7 @@ void main() { }); group('fromMediaError', () { - testWidgets('with aborted error code', (tester) async { + testWidgets('with aborted error code', (WidgetTester tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_ABORTED), @@ -137,7 +137,7 @@ void main() { ); }); - testWidgets('with network error code', (tester) async { + testWidgets('with network error code', (WidgetTester tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_NETWORK), @@ -146,7 +146,7 @@ void main() { ); }); - testWidgets('with decode error code', (tester) async { + testWidgets('with decode error code', (WidgetTester tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_DECODE), @@ -155,7 +155,8 @@ void main() { ); }); - testWidgets('with source not supported error code', (tester) async { + testWidgets('with source not supported error code', + (WidgetTester tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED), @@ -164,7 +165,7 @@ void main() { ); }); - testWidgets('with unknown error code', (tester) async { + testWidgets('with unknown error code', (WidgetTester tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(5), diff --git a/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart index 36ecb3e47f31..07252be5c7a2 100644 --- a/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart @@ -10,14 +10,14 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraMetadata', () { - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( - CameraMetadata( + const CameraMetadata( deviceId: 'deviceId', facingMode: 'environment', ), equals( - CameraMetadata( + const CameraMetadata( deviceId: 'deviceId', facingMode: 'environment', ), 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 a74ba3088394..ee63d87e9f07 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 @@ -10,9 +10,9 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraOptions', () { - testWidgets('serializes correctly', (tester) async { - final cameraOptions = CameraOptions( - audio: AudioConstraints(enabled: true), + testWidgets('serializes correctly', (WidgetTester tester) async { + final CameraOptions cameraOptions = CameraOptions( + audio: const AudioConstraints(enabled: true), video: VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.user), ), @@ -20,31 +20,35 @@ void main() { expect( cameraOptions.toJson(), - equals({ + equals({ 'audio': cameraOptions.audio.toJson(), 'video': cameraOptions.video.toJson(), }), ); }); - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( CameraOptions( - audio: AudioConstraints(enabled: false), + audio: const AudioConstraints(enabled: false), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.environment), - width: VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), - height: VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), + width: + const VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), + height: + const VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), deviceId: 'deviceId', ), ), equals( CameraOptions( - audio: AudioConstraints(enabled: false), + audio: const AudioConstraints(enabled: false), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.environment), - width: VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), - height: VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), + width: const VideoSizeConstraint( + minimum: 10, ideal: 15, maximum: 20), + height: const VideoSizeConstraint( + minimum: 15, ideal: 20, maximum: 25), deviceId: 'deviceId', ), ), @@ -54,56 +58,60 @@ void main() { }); group('AudioConstraints', () { - testWidgets('serializes correctly', (tester) async { + testWidgets('serializes correctly', (WidgetTester tester) async { expect( - AudioConstraints(enabled: true).toJson(), + const AudioConstraints(enabled: true).toJson(), equals(true), ); }); - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( - AudioConstraints(enabled: true), - equals(AudioConstraints(enabled: true)), + const AudioConstraints(enabled: true), + equals(const AudioConstraints(enabled: true)), ); }); }); group('VideoConstraints', () { - testWidgets('serializes correctly', (tester) async { - final videoConstraints = VideoConstraints( + testWidgets('serializes correctly', (WidgetTester tester) async { + final VideoConstraints videoConstraints = VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.user), - width: VideoSizeConstraint(ideal: 100, maximum: 100), - height: VideoSizeConstraint(ideal: 50, maximum: 50), + width: const VideoSizeConstraint(ideal: 100, maximum: 100), + height: const VideoSizeConstraint(ideal: 50, maximum: 50), deviceId: 'deviceId', ); expect( videoConstraints.toJson(), - equals({ + equals({ 'facingMode': videoConstraints.facingMode!.toJson(), 'width': videoConstraints.width!.toJson(), 'height': videoConstraints.height!.toJson(), - 'deviceId': { + 'deviceId': { 'exact': 'deviceId', } }), ); }); - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.environment), - width: VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), - height: VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), + width: + const VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), + height: + const VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), deviceId: 'deviceId', ), equals( VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.environment), - width: VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), - height: VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), + width: const VideoSizeConstraint( + minimum: 90, ideal: 100, maximum: 100), + height: + const VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), deviceId: 'deviceId', ), ), @@ -115,23 +123,23 @@ void main() { group('ideal', () { testWidgets( 'serializes correctly ' - 'for environment camera type', (tester) async { + 'for environment camera type', (WidgetTester tester) async { expect( FacingModeConstraint(CameraType.environment).toJson(), - equals({'ideal': 'environment'}), + equals({'ideal': 'environment'}), ); }); testWidgets( 'serializes correctly ' - 'for user camera type', (tester) async { + 'for user camera type', (WidgetTester tester) async { expect( FacingModeConstraint(CameraType.user).toJson(), - equals({'ideal': 'user'}), + equals({'ideal': 'user'}), ); }); - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( FacingModeConstraint(CameraType.user), equals(FacingModeConstraint(CameraType.user)), @@ -142,23 +150,23 @@ void main() { group('exact', () { testWidgets( 'serializes correctly ' - 'for environment camera type', (tester) async { + 'for environment camera type', (WidgetTester tester) async { expect( FacingModeConstraint.exact(CameraType.environment).toJson(), - equals({'exact': 'environment'}), + equals({'exact': 'environment'}), ); }); testWidgets( 'serializes correctly ' - 'for user camera type', (tester) async { + 'for user camera type', (WidgetTester tester) async { expect( FacingModeConstraint.exact(CameraType.user).toJson(), - equals({'exact': 'user'}), + equals({'exact': 'user'}), ); }); - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( FacingModeConstraint.exact(CameraType.environment), equals(FacingModeConstraint.exact(CameraType.environment)), @@ -168,14 +176,14 @@ void main() { }); group('VideoSizeConstraint ', () { - testWidgets('serializes correctly', (tester) async { + testWidgets('serializes correctly', (WidgetTester tester) async { expect( - VideoSizeConstraint( + const VideoSizeConstraint( minimum: 200, ideal: 400, maximum: 400, ).toJson(), - equals({ + equals({ 'min': 200, 'ideal': 400, 'max': 400, @@ -183,15 +191,15 @@ void main() { ); }); - testWidgets('supports value equality', (tester) async { + testWidgets('supports value equality', (WidgetTester tester) async { expect( - VideoSizeConstraint( + const VideoSizeConstraint( minimum: 100, ideal: 200, maximum: 300, ), equals( - VideoSizeConstraint( + const VideoSizeConstraint( minimum: 100, ideal: 200, maximum: 300, 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 346ab26237ea..96debbf0f491 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 @@ -3,8 +3,10 @@ // found in the LICENSE file. import 'dart:html'; -import 'dart:ui'; 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'; import 'package:camera_web/src/camera.dart'; @@ -22,7 +24,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraService', () { - const cameraId = 0; + const int cameraId = 0; late Window window; late Navigator navigator; @@ -40,10 +42,10 @@ void main() { when(() => navigator.mediaDevices).thenReturn(mediaDevices); // Mock JsUtil to return the real getProperty from dart:js_util. - when(() => jsUtil.getProperty(any(), any())).thenAnswer( - (invocation) => js_util.getProperty( - invocation.positionalArguments[0], - invocation.positionalArguments[1], + when(() => jsUtil.getProperty(any(), any())).thenAnswer( + (Invocation invocation) => js_util.getProperty( + invocation.positionalArguments[0] as Object, + invocation.positionalArguments[1] as Object, ), ); @@ -53,14 +55,14 @@ void main() { group('getMediaStreamForOptions', () { testWidgets( 'calls MediaDevices.getUserMedia ' - 'with provided options', (tester) async { + 'with provided options', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) - .thenAnswer((_) async => FakeMediaStream([])); + .thenAnswer((_) async => FakeMediaStream([])); - final options = CameraOptions( + final CameraOptions options = CameraOptions( video: VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.user), - width: VideoSizeConstraint(ideal: 200), + width: const VideoSizeConstraint(ideal: 200), ), ); @@ -74,14 +76,14 @@ void main() { testWidgets( 'throws PlatformException ' 'with notSupported error ' - 'when there are no media devices', (tester) async { + 'when there are no media devices', (WidgetTester tester) async { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => cameraService.getMediaStreamForOptions(CameraOptions()), + () => cameraService.getMediaStreamForOptions(const CameraOptions()), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notSupported.toString(), ), @@ -93,19 +95,21 @@ void main() { testWidgets( 'with notFound error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with NotFoundError', (tester) async { + 'with NotFoundError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('NotFoundError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.notFound), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.notFound), ), ); }); @@ -113,19 +117,21 @@ void main() { testWidgets( 'with notFound error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with DevicesNotFoundError', (tester) async { + 'with DevicesNotFoundError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('DevicesNotFoundError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.notFound), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.notFound), ), ); }); @@ -133,19 +139,21 @@ void main() { testWidgets( 'with notReadable error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with NotReadableError', (tester) async { + 'with NotReadableError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('NotReadableError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.notReadable), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.notReadable), ), ); }); @@ -153,19 +161,21 @@ void main() { testWidgets( 'with notReadable error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with TrackStartError', (tester) async { + 'with TrackStartError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('TrackStartError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.notReadable), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.notReadable), ), ); }); @@ -173,20 +183,21 @@ void main() { testWidgets( 'with overconstrained error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with OverconstrainedError', (tester) async { + 'with OverconstrainedError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('OverconstrainedError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having( - (e) => e.code, 'code', CameraErrorCode.overconstrained), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.overconstrained), ), ); }); @@ -194,20 +205,21 @@ void main() { testWidgets( 'with overconstrained error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with ConstraintNotSatisfiedError', (tester) async { + 'with ConstraintNotSatisfiedError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having( - (e) => e.code, 'code', CameraErrorCode.overconstrained), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.overconstrained), ), ); }); @@ -215,20 +227,21 @@ void main() { testWidgets( 'with permissionDenied error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with NotAllowedError', (tester) async { + 'with NotAllowedError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('NotAllowedError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having( - (e) => e.code, 'code', CameraErrorCode.permissionDenied), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.permissionDenied), ), ); }); @@ -236,20 +249,21 @@ void main() { testWidgets( 'with permissionDenied error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with PermissionDeniedError', (tester) async { + 'with PermissionDeniedError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('PermissionDeniedError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having( - (e) => e.code, 'code', CameraErrorCode.permissionDenied), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.permissionDenied), ), ); }); @@ -257,19 +271,21 @@ void main() { testWidgets( 'with type error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with TypeError', (tester) async { + 'with TypeError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('TypeError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.type), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.type), ), ); }); @@ -277,19 +293,21 @@ void main() { testWidgets( 'with abort error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with AbortError', (tester) async { + 'with AbortError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('AbortError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.abort), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.abort), ), ); }); @@ -297,19 +315,21 @@ void main() { testWidgets( 'with security error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with SecurityError', (tester) async { + 'with SecurityError', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('SecurityError')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.security), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.security), ), ); }); @@ -317,19 +337,21 @@ void main() { testWidgets( 'with unknown error ' 'when MediaDevices.getUserMedia throws DomException ' - 'with an unknown error', (tester) async { + 'with an unknown error', (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())) .thenThrow(FakeDomException('Unknown')); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.unknown), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.unknown), ), ); }); @@ -337,18 +359,20 @@ void main() { testWidgets( 'with unknown error ' 'when MediaDevices.getUserMedia throws an unknown exception', - (tester) async { + (WidgetTester tester) async { when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); expect( () => cameraService.getMediaStreamForOptions( - CameraOptions(), + const CameraOptions(), cameraId: cameraId, ), throwsA( isA() - .having((e) => e.cameraId, 'cameraId', cameraId) - .having((e) => e.code, 'code', CameraErrorCode.unknown), + .having((CameraWebException e) => e.cameraId, 'cameraId', + cameraId) + .having((CameraWebException e) => e.code, 'code', + CameraErrorCode.unknown), ), ); }); @@ -361,7 +385,10 @@ void main() { setUp(() { camera = MockCamera(); - videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoTracks = [ + MockMediaStreamTrack(), + MockMediaStreamTrack() + ]; when(() => camera.textureId).thenReturn(0); when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks)); @@ -371,20 +398,21 @@ void main() { testWidgets( 'returns the zoom level capability ' - 'based on the first video track', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'based on the first video track', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'zoom': true, }); - when(videoTracks.first.getCapabilities).thenReturn({ - 'zoom': js_util.jsify({ + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': js_util.jsify({ 'min': 100, 'max': 400, 'step': 2, }), }); - final zoomLevelCapability = + final ZoomLevelCapability zoomLevelCapability = cameraService.getZoomLevelCapabilityForCamera(camera); expect(zoomLevelCapability.minimum, equals(100.0)); @@ -395,7 +423,7 @@ void main() { group('throws CameraWebException', () { testWidgets( 'with zoomLevelNotSupported error ' - 'when there are no media devices', (tester) async { + 'when there are no media devices', (WidgetTester tester) async { when(() => navigator.mediaDevices).thenReturn(null); expect( @@ -403,12 +431,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', camera.textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.zoomLevelNotSupported, ), @@ -419,13 +447,14 @@ void main() { testWidgets( 'with zoomLevelNotSupported error ' 'when the zoom level is not supported ' - 'in the browser', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'in the browser', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'zoom': false, }); - when(videoTracks.first.getCapabilities).thenReturn({ - 'zoom': { + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': { 'min': 100, 'max': 400, 'step': 2, @@ -437,12 +466,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', camera.textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.zoomLevelNotSupported, ), @@ -453,24 +482,26 @@ void main() { testWidgets( 'with zoomLevelNotSupported error ' 'when the zoom level is not supported ' - 'by the camera', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'by the camera', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'zoom': true, }); - when(videoTracks.first.getCapabilities).thenReturn({}); + when(videoTracks.first.getCapabilities) + .thenReturn({}); expect( () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', camera.textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.zoomLevelNotSupported, ), @@ -480,25 +511,28 @@ void main() { testWidgets( 'with notStarted error ' - 'when the camera stream has not been initialized', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'when the camera stream has not been initialized', + (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'zoom': true, }); // Create a camera stream with no video tracks. - when(() => camera.stream).thenReturn(FakeMediaStream([])); + when(() => camera.stream) + .thenReturn(FakeMediaStream([])); expect( () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', camera.textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.notStarted, ), @@ -516,7 +550,7 @@ void main() { testWidgets( 'throws PlatformException ' 'with notSupported error ' - 'when there are no media devices', (tester) async { + 'when there are no media devices', (WidgetTester tester) async { when(() => navigator.mediaDevices).thenReturn(null); expect( @@ -524,7 +558,7 @@ void main() { cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notSupported.toString(), ), @@ -534,12 +568,13 @@ void main() { testWidgets( 'returns null ' - 'when the facing mode is not supported', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'when the facing mode is not supported', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'facingMode': false, }); - final facingMode = + final String? facingMode = cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()); expect(facingMode, isNull); @@ -554,17 +589,19 @@ void main() { when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) .thenReturn(true); - when(mediaDevices.getSupportedConstraints).thenReturn({ + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'facingMode': true, }); }); testWidgets( 'returns an appropriate facing mode ' - 'based on the video track settings', (tester) async { - when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); + 'based on the video track settings', (WidgetTester tester) async { + when(videoTrack.getSettings) + .thenReturn({'facingMode': 'user'}); - final facingMode = + final String? facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); expect(facingMode, equals('user')); @@ -573,16 +610,17 @@ void main() { testWidgets( 'returns an appropriate facing mode ' 'based on the video track capabilities ' - 'when the facing mode setting is empty', (tester) async { - when(videoTrack.getSettings).thenReturn({}); - when(videoTrack.getCapabilities).thenReturn({ - 'facingMode': ['environment', 'left'] + 'when the facing mode setting is empty', + (WidgetTester tester) async { + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({ + 'facingMode': ['environment', 'left'] }); when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) .thenReturn(true); - final facingMode = + final String? facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); expect(facingMode, equals('environment')); @@ -591,11 +629,12 @@ void main() { testWidgets( 'returns null ' 'when the facing mode setting ' - 'and capabilities are empty', (tester) async { - when(videoTrack.getSettings).thenReturn({}); - when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); + 'and capabilities are empty', (WidgetTester tester) async { + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities) + .thenReturn({'facingMode': []}); - final facingMode = + final String? facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); expect(facingMode, isNull); @@ -604,13 +643,14 @@ void main() { testWidgets( 'returns null ' 'when the facing mode setting is empty and ' - 'the video track capabilities are not supported', (tester) async { - when(videoTrack.getSettings).thenReturn({}); + 'the video track capabilities are not supported', + (WidgetTester tester) async { + when(videoTrack.getSettings).thenReturn({}); when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) .thenReturn(false); - final facingMode = + final String? facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); expect(facingMode, isNull); @@ -621,7 +661,7 @@ void main() { group('mapFacingModeToLensDirection', () { testWidgets( 'returns front ' - 'when the facing mode is user', (tester) async { + 'when the facing mode is user', (WidgetTester tester) async { expect( cameraService.mapFacingModeToLensDirection('user'), equals(CameraLensDirection.front), @@ -630,7 +670,7 @@ void main() { testWidgets( 'returns back ' - 'when the facing mode is environment', (tester) async { + 'when the facing mode is environment', (WidgetTester tester) async { expect( cameraService.mapFacingModeToLensDirection('environment'), equals(CameraLensDirection.back), @@ -639,7 +679,7 @@ void main() { testWidgets( 'returns external ' - 'when the facing mode is left', (tester) async { + 'when the facing mode is left', (WidgetTester tester) async { expect( cameraService.mapFacingModeToLensDirection('left'), equals(CameraLensDirection.external), @@ -648,7 +688,7 @@ void main() { testWidgets( 'returns external ' - 'when the facing mode is right', (tester) async { + 'when the facing mode is right', (WidgetTester tester) async { expect( cameraService.mapFacingModeToLensDirection('right'), equals(CameraLensDirection.external), @@ -659,7 +699,7 @@ void main() { group('mapFacingModeToCameraType', () { testWidgets( 'returns user ' - 'when the facing mode is user', (tester) async { + 'when the facing mode is user', (WidgetTester tester) async { expect( cameraService.mapFacingModeToCameraType('user'), equals(CameraType.user), @@ -668,7 +708,7 @@ void main() { testWidgets( 'returns environment ' - 'when the facing mode is environment', (tester) async { + 'when the facing mode is environment', (WidgetTester tester) async { expect( cameraService.mapFacingModeToCameraType('environment'), equals(CameraType.environment), @@ -677,7 +717,7 @@ void main() { testWidgets( 'returns user ' - 'when the facing mode is left', (tester) async { + 'when the facing mode is left', (WidgetTester tester) async { expect( cameraService.mapFacingModeToCameraType('left'), equals(CameraType.user), @@ -686,7 +726,7 @@ void main() { testWidgets( 'returns user ' - 'when the facing mode is right', (tester) async { + 'when the facing mode is right', (WidgetTester tester) async { expect( cameraService.mapFacingModeToCameraType('right'), equals(CameraType.user), @@ -697,55 +737,57 @@ void main() { group('mapResolutionPresetToSize', () { testWidgets( 'returns 4096x2160 ' - 'when the resolution preset is max', (tester) async { + 'when the resolution preset is max', (WidgetTester tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.max), - equals(Size(4096, 2160)), + equals(const Size(4096, 2160)), ); }); testWidgets( 'returns 4096x2160 ' - 'when the resolution preset is ultraHigh', (tester) async { + 'when the resolution preset is ultraHigh', + (WidgetTester tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), - equals(Size(4096, 2160)), + equals(const Size(4096, 2160)), ); }); testWidgets( 'returns 1920x1080 ' - 'when the resolution preset is veryHigh', (tester) async { + 'when the resolution preset is veryHigh', + (WidgetTester tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.veryHigh), - equals(Size(1920, 1080)), + equals(const Size(1920, 1080)), ); }); testWidgets( 'returns 1280x720 ' - 'when the resolution preset is high', (tester) async { + 'when the resolution preset is high', (WidgetTester tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.high), - equals(Size(1280, 720)), + equals(const Size(1280, 720)), ); }); testWidgets( 'returns 720x480 ' - 'when the resolution preset is medium', (tester) async { + 'when the resolution preset is medium', (WidgetTester tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.medium), - equals(Size(720, 480)), + equals(const Size(720, 480)), ); }); testWidgets( 'returns 320x240 ' - 'when the resolution preset is low', (tester) async { + 'when the resolution preset is low', (WidgetTester tester) async { expect( cameraService.mapResolutionPresetToSize(ResolutionPreset.low), - equals(Size(320, 240)), + equals(const Size(320, 240)), ); }); }); @@ -753,7 +795,8 @@ void main() { group('mapDeviceOrientationToOrientationType', () { testWidgets( 'returns portraitPrimary ' - 'when the device orientation is portraitUp', (tester) async { + 'when the device orientation is portraitUp', + (WidgetTester tester) async { expect( cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.portraitUp, @@ -764,7 +807,8 @@ void main() { testWidgets( 'returns landscapePrimary ' - 'when the device orientation is landscapeLeft', (tester) async { + 'when the device orientation is landscapeLeft', + (WidgetTester tester) async { expect( cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeLeft, @@ -775,7 +819,8 @@ void main() { testWidgets( 'returns portraitSecondary ' - 'when the device orientation is portraitDown', (tester) async { + 'when the device orientation is portraitDown', + (WidgetTester tester) async { expect( cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.portraitDown, @@ -786,7 +831,8 @@ void main() { testWidgets( 'returns landscapeSecondary ' - 'when the device orientation is landscapeRight', (tester) async { + 'when the device orientation is landscapeRight', + (WidgetTester tester) async { expect( cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, @@ -799,7 +845,8 @@ void main() { group('mapOrientationTypeToDeviceOrientation', () { testWidgets( 'returns portraitUp ' - 'when the orientation type is portraitPrimary', (tester) async { + 'when the orientation type is portraitPrimary', + (WidgetTester tester) async { expect( cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitPrimary, @@ -810,7 +857,8 @@ void main() { testWidgets( 'returns landscapeLeft ' - 'when the orientation type is landscapePrimary', (tester) async { + 'when the orientation type is landscapePrimary', + (WidgetTester tester) async { expect( cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapePrimary, @@ -821,7 +869,8 @@ void main() { testWidgets( 'returns portraitDown ' - 'when the orientation type is portraitSecondary', (tester) async { + 'when the orientation type is portraitSecondary', + (WidgetTester tester) async { expect( cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, @@ -832,7 +881,8 @@ void main() { testWidgets( 'returns portraitDown ' - 'when the orientation type is portraitSecondary', (tester) async { + 'when the orientation type is portraitSecondary', + (WidgetTester tester) async { expect( cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, @@ -843,7 +893,8 @@ void main() { testWidgets( 'returns landscapeRight ' - 'when the orientation type is landscapeSecondary', (tester) async { + 'when the orientation type is landscapeSecondary', + (WidgetTester tester) async { expect( cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapeSecondary, @@ -854,7 +905,7 @@ void main() { testWidgets( 'returns portraitUp ' - 'for an unknown orientation type', (tester) async { + 'for an unknown orientation type', (WidgetTester tester) async { expect( cameraService.mapOrientationTypeToDeviceOrientation( 'unknown', 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 3a25e33c5398..50451b9778af 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -21,7 +21,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Camera', () { - const textureId = 1; + const int textureId = 1; late Window window; late Navigator navigator; @@ -40,7 +40,8 @@ void main() { cameraService = MockCameraService(); - final videoElement = getVideoElementWithBlankStream(Size(10, 10)); + final VideoElement videoElement = + getVideoElementWithBlankStream(const Size(10, 10)); mediaStream = videoElement.captureStream(); when( @@ -48,25 +49,25 @@ void main() { any(), cameraId: any(named: 'cameraId'), ), - ).thenAnswer((_) => Future.value(mediaStream)); + ).thenAnswer((_) => Future.value(mediaStream)); }); setUpAll(() { - registerFallbackValue(MockCameraOptions()); + registerFallbackValue(MockCameraOptions()); }); group('initialize', () { testWidgets( 'calls CameraService.getMediaStreamForOptions ' - 'with provided options', (tester) async { - final options = CameraOptions( + 'with provided options', (WidgetTester tester) async { + final CameraOptions options = CameraOptions( video: VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.user), - width: VideoSizeConstraint(ideal: 200), + width: const VideoSizeConstraint(ideal: 200), ), ); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, options: options, cameraService: cameraService, @@ -84,15 +85,16 @@ void main() { testWidgets( 'creates a video element ' - 'with correct properties', (tester) async { - const audioConstraints = AudioConstraints(enabled: true); - final videoConstraints = VideoConstraints( + 'with correct properties', (WidgetTester tester) async { + const AudioConstraints audioConstraints = + AudioConstraints(enabled: true); + final VideoConstraints videoConstraints = VideoConstraints( facingMode: FacingModeConstraint( CameraType.user, ), ); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, options: CameraOptions( audio: audioConstraints, @@ -119,14 +121,14 @@ void main() { testWidgets( 'flips the video element horizontally ' - 'for a back camera', (tester) async { - final videoConstraints = VideoConstraints( + 'for a back camera', (WidgetTester tester) async { + final VideoConstraints videoConstraints = VideoConstraints( facingMode: FacingModeConstraint( CameraType.environment, ), ); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, options: CameraOptions( video: videoConstraints, @@ -141,8 +143,8 @@ void main() { testWidgets( 'creates a wrapping div element ' - 'with correct properties', (tester) async { - final camera = Camera( + 'with correct properties', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -154,8 +156,8 @@ void main() { expect(camera.divElement.children, contains(camera.videoElement)); }); - testWidgets('initializes the camera stream', (tester) async { - final camera = Camera( + testWidgets('initializes the camera stream', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -167,13 +169,15 @@ void main() { testWidgets( 'throws an exception ' - 'when CameraService.getMediaStreamForOptions throws', (tester) async { - final exception = Exception('A media stream exception occured.'); + 'when CameraService.getMediaStreamForOptions throws', + (WidgetTester tester) async { + final Exception exception = + Exception('A media stream exception occured.'); when(() => cameraService.getMediaStreamForOptions(any(), cameraId: any(named: 'cameraId'))).thenThrow(exception); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -186,18 +190,20 @@ void main() { }); group('play', () { - testWidgets('starts playing the video element', (tester) async { - var startedPlaying = false; + testWidgets('starts playing the video element', + (WidgetTester tester) async { + bool startedPlaying = false; - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); await camera.initialize(); - final cameraPlaySubscription = - camera.videoElement.onPlay.listen((event) => startedPlaying = true); + final StreamSubscription cameraPlaySubscription = camera + .videoElement.onPlay + .listen((Event event) => startedPlaying = true); await camera.play(); @@ -209,14 +215,14 @@ void main() { testWidgets( 'initializes the camera stream ' 'from CameraService.getMediaStreamForOptions ' - 'if it does not exist', (tester) async { - final options = CameraOptions( + 'if it does not exist', (WidgetTester tester) async { + const CameraOptions options = CameraOptions( video: VideoConstraints( width: VideoSizeConstraint(ideal: 100), ), ); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, options: options, cameraService: cameraService, @@ -244,8 +250,8 @@ void main() { }); group('pause', () { - testWidgets('pauses the camera stream', (tester) async { - final camera = Camera( + testWidgets('pauses the camera stream', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -262,8 +268,8 @@ void main() { }); group('stop', () { - testWidgets('resets the camera stream', (tester) async { - final camera = Camera( + testWidgets('resets the camera stream', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -279,8 +285,8 @@ void main() { }); group('takePicture', () { - testWidgets('returns a captured picture', (tester) async { - final camera = Camera( + testWidgets('returns a captured picture', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -288,7 +294,7 @@ void main() { await camera.initialize(); await camera.play(); - final pictureFile = await camera.takePicture(); + final XFile pictureFile = await camera.takePicture(); expect(pictureFile, isNotNull); }); @@ -301,22 +307,25 @@ void main() { late VideoElement videoElement; setUp(() { - videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoTracks = [ + MockMediaStreamTrack(), + MockMediaStreamTrack() + ]; videoStream = FakeMediaStream(videoTracks); - videoElement = getVideoElementWithBlankStream(Size(100, 100)) + videoElement = getVideoElementWithBlankStream(const Size(100, 100)) ..muted = true; when(() => videoTracks.first.applyConstraints(any())) - .thenAnswer((_) async => {}); + .thenAnswer((_) async => {}); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': true, }); }); - testWidgets('if the flash mode is auto', (tester) async { - final camera = Camera( + testWidgets('if the flash mode is auto', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -327,31 +336,31 @@ void main() { await camera.play(); - final _ = await camera.takePicture(); + final XFile _ = await camera.takePicture(); verify( - () => videoTracks.first.applyConstraints({ - "advanced": [ - { - "torch": true, + () => videoTracks.first.applyConstraints({ + 'advanced': [ + { + 'torch': true, } ] }), ).called(1); verify( - () => videoTracks.first.applyConstraints({ - "advanced": [ - { - "torch": false, + () => videoTracks.first.applyConstraints({ + 'advanced': [ + { + 'torch': false, } ] }), ).called(1); }); - testWidgets('if the flash mode is always', (tester) async { - final camera = Camera( + testWidgets('if the flash mode is always', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -362,23 +371,23 @@ void main() { await camera.play(); - final _ = await camera.takePicture(); + final XFile _ = await camera.takePicture(); verify( - () => videoTracks.first.applyConstraints({ - "advanced": [ - { - "torch": true, + () => videoTracks.first.applyConstraints({ + 'advanced': [ + { + 'torch': true, } ] }), ).called(1); verify( - () => videoTracks.first.applyConstraints({ - "advanced": [ - { - "torch": false, + () => videoTracks.first.applyConstraints({ + 'advanced': [ + { + 'torch': false, } ] }), @@ -390,13 +399,15 @@ void main() { group('getVideoSize', () { testWidgets( 'returns a size ' - 'based on the first video track settings', (tester) async { - const videoSize = Size(1280, 720); + 'based on the first video track settings', + (WidgetTester tester) async { + const Size videoSize = Size(1280, 720); - final videoElement = getVideoElementWithBlankStream(videoSize); + final VideoElement videoElement = + getVideoElementWithBlankStream(videoSize); mediaStream = videoElement.captureStream(); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -411,12 +422,12 @@ void main() { testWidgets( 'returns Size.zero ' - 'if the camera is missing video tracks', (tester) async { + 'if the camera is missing video tracks', (WidgetTester tester) async { // Create a video stream with no video tracks. - final videoElement = VideoElement(); + final VideoElement videoElement = VideoElement(); mediaStream = videoElement.captureStream(); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -435,32 +446,37 @@ void main() { late MediaStream videoStream; setUp(() { - videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoTracks = [ + MockMediaStreamTrack(), + MockMediaStreamTrack() + ]; videoStream = FakeMediaStream(videoTracks); when(() => videoTracks.first.applyConstraints(any())) - .thenAnswer((_) async => {}); + .thenAnswer((_) async => {}); - when(videoTracks.first.getCapabilities).thenReturn({}); + when(videoTracks.first.getCapabilities) + .thenReturn({}); }); - testWidgets('sets the camera flash mode', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + testWidgets('sets the camera flash mode', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'torch': true, }); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': true, }); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) ..window = window ..stream = videoStream; - const flashMode = FlashMode.always; + const FlashMode flashMode = FlashMode.always; camera.setFlashMode(flashMode); @@ -472,16 +488,17 @@ void main() { testWidgets( 'enables the torch mode ' - 'if the flash mode is torch', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'if the flash mode is torch', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'torch': true, }); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': true, }); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -491,10 +508,10 @@ void main() { camera.setFlashMode(FlashMode.torch); verify( - () => videoTracks.first.applyConstraints({ - "advanced": [ - { - "torch": true, + () => videoTracks.first.applyConstraints({ + 'advanced': [ + { + 'torch': true, } ] }), @@ -503,16 +520,17 @@ void main() { testWidgets( 'disables the torch mode ' - 'if the flash mode is not torch', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'if the flash mode is not torch', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'torch': true, }); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': true, }); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -522,10 +540,10 @@ void main() { camera.setFlashMode(FlashMode.auto); verify( - () => videoTracks.first.applyConstraints({ - "advanced": [ - { - "torch": false, + () => videoTracks.first.applyConstraints({ + 'advanced': [ + { + 'torch': false, } ] }), @@ -535,10 +553,10 @@ void main() { group('throws a CameraWebException', () { testWidgets( 'with torchModeNotSupported error ' - 'when there are no media devices', (tester) async { + 'when there are no media devices', (WidgetTester tester) async { when(() => navigator.mediaDevices).thenReturn(null); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -550,12 +568,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.torchModeNotSupported, ), @@ -566,16 +584,17 @@ void main() { testWidgets( 'with torchModeNotSupported error ' 'when the torch mode is not supported ' - 'in the browser', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'in the browser', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'torch': false, }); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': true, }); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -587,12 +606,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.torchModeNotSupported, ), @@ -603,16 +622,17 @@ void main() { testWidgets( 'with torchModeNotSupported error ' 'when the torch mode is not supported ' - 'by the camera', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'by the camera', (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'torch': true, }); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': false, }); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ) @@ -624,12 +644,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.torchModeNotSupported, ), @@ -639,16 +659,18 @@ void main() { testWidgets( 'with notStarted error ' - 'when the camera stream has not been initialized', (tester) async { - when(mediaDevices.getSupportedConstraints).thenReturn({ + 'when the camera stream has not been initialized', + (WidgetTester tester) async { + when(mediaDevices.getSupportedConstraints) + .thenReturn({ 'torch': true, }); - when(videoTracks.first.getCapabilities).thenReturn({ + when(videoTracks.first.getCapabilities).thenReturn({ 'torch': true, }); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, )..window = window; @@ -658,12 +680,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.notStarted, ), @@ -678,13 +700,13 @@ void main() { testWidgets( 'returns maximum ' 'from CameraService.getZoomLevelCapabilityForCamera', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final zoomLevelCapability = ZoomLevelCapability( + final ZoomLevelCapability zoomLevelCapability = ZoomLevelCapability( minimum: 50.0, maximum: 100.0, videoTrack: MockMediaStreamTrack(), @@ -693,7 +715,7 @@ void main() { when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); - final maximumZoomLevel = camera.getMaxZoomLevel(); + final double maximumZoomLevel = camera.getMaxZoomLevel(); verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .called(1); @@ -709,13 +731,13 @@ void main() { testWidgets( 'returns minimum ' 'from CameraService.getZoomLevelCapabilityForCamera', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final zoomLevelCapability = ZoomLevelCapability( + final ZoomLevelCapability zoomLevelCapability = ZoomLevelCapability( minimum: 50.0, maximum: 100.0, videoTrack: MockMediaStreamTrack(), @@ -724,7 +746,7 @@ void main() { when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); - final minimumZoomLevel = camera.getMinZoomLevel(); + final double minimumZoomLevel = camera.getMinZoomLevel(); verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .called(1); @@ -740,15 +762,15 @@ void main() { testWidgets( 'applies zoom on the video track ' 'from CameraService.getZoomLevelCapabilityForCamera', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final videoTrack = MockMediaStreamTrack(); + final MockMediaStreamTrack videoTrack = MockMediaStreamTrack(); - final zoomLevelCapability = ZoomLevelCapability( + final ZoomLevelCapability zoomLevelCapability = ZoomLevelCapability( minimum: 50.0, maximum: 100.0, videoTrack: videoTrack, @@ -760,14 +782,14 @@ void main() { when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); - const zoom = 75.0; + const double zoom = 75.0; camera.setZoomLevel(zoom); verify( - () => videoTrack.applyConstraints({ - "advanced": [ - { + () => videoTrack.applyConstraints({ + 'advanced': [ + { ZoomLevelCapability.constraintName: zoom, } ] @@ -778,13 +800,14 @@ void main() { group('throws a CameraWebException', () { testWidgets( 'with zoomLevelInvalid error ' - 'when the provided zoom level is below minimum', (tester) async { - final camera = Camera( + 'when the provided zoom level is below minimum', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final zoomLevelCapability = ZoomLevelCapability( + final ZoomLevelCapability zoomLevelCapability = ZoomLevelCapability( minimum: 50.0, maximum: 100.0, videoTrack: MockMediaStreamTrack(), @@ -798,12 +821,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.zoomLevelInvalid, ), @@ -812,13 +835,14 @@ void main() { testWidgets( 'with zoomLevelInvalid error ' - 'when the provided zoom level is below minimum', (tester) async { - final camera = Camera( + 'when the provided zoom level is below minimum', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final zoomLevelCapability = ZoomLevelCapability( + final ZoomLevelCapability zoomLevelCapability = ZoomLevelCapability( minimum: 50.0, maximum: 100.0, videoTrack: MockMediaStreamTrack(), @@ -832,12 +856,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.zoomLevelInvalid, ), @@ -851,25 +875,26 @@ void main() { group('getLensDirection', () { testWidgets( 'returns a lens direction ' - 'based on the first video track settings', (tester) async { - final videoElement = MockVideoElement(); + 'based on the first video track settings', + (WidgetTester tester) async { + final MockVideoElement videoElement = MockVideoElement(); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, )..videoElement = videoElement; - final firstVideoTrack = MockMediaStreamTrack(); + final MockMediaStreamTrack firstVideoTrack = MockMediaStreamTrack(); when(() => videoElement.srcObject).thenReturn( - FakeMediaStream([ + FakeMediaStream([ firstVideoTrack, MockMediaStreamTrack(), ]), ); when(firstVideoTrack.getSettings) - .thenReturn({'facingMode': 'environment'}); + .thenReturn({'facingMode': 'environment'}); when(() => cameraService.mapFacingModeToLensDirection('environment')) .thenReturn(CameraLensDirection.external); @@ -883,24 +908,24 @@ void main() { testWidgets( 'returns null ' 'if the first video track is missing the facing mode', - (tester) async { - final videoElement = MockVideoElement(); + (WidgetTester tester) async { + final MockVideoElement videoElement = MockVideoElement(); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, )..videoElement = videoElement; - final firstVideoTrack = MockMediaStreamTrack(); + final MockMediaStreamTrack firstVideoTrack = MockMediaStreamTrack(); when(() => videoElement.srcObject).thenReturn( - FakeMediaStream([ + FakeMediaStream([ firstVideoTrack, MockMediaStreamTrack(), ]), ); - when(firstVideoTrack.getSettings).thenReturn({}); + when(firstVideoTrack.getSettings).thenReturn({}); expect( camera.getLensDirection(), @@ -910,12 +935,12 @@ void main() { testWidgets( 'returns null ' - 'if the camera is missing video tracks', (tester) async { + 'if the camera is missing video tracks', (WidgetTester tester) async { // Create a video stream with no video tracks. - final videoElement = VideoElement(); + final VideoElement videoElement = VideoElement(); mediaStream = videoElement.captureStream(); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -930,8 +955,8 @@ void main() { }); group('getViewType', () { - testWidgets('returns a correct view type', (tester) async { - final camera = Camera( + testWidgets('returns a correct view type', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -946,7 +971,7 @@ void main() { }); group('video recording', () { - const supportedVideoType = 'video/webm'; + const String supportedVideoType = 'video/webm'; late MediaRecorder mediaRecorder; @@ -956,14 +981,14 @@ void main() { mediaRecorder = MockMediaRecorder(); when(() => mediaRecorder.onError) - .thenAnswer((_) => const Stream.empty()); + .thenAnswer((_) => const Stream.empty()); }); group('startVideoRecording', () { testWidgets( 'creates a media recorder ' - 'with appropriate options', (tester) async { - final camera = Camera( + 'with appropriate options', (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, )..isVideoTypeSupported = isVideoTypeSupported; @@ -990,8 +1015,8 @@ void main() { }); testWidgets('listens to the media recorder data events', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1009,8 +1034,8 @@ void main() { }); testWidgets('listens to the media recorder stop events', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1027,8 +1052,8 @@ void main() { ).called(1); }); - testWidgets('starts a video recording', (tester) async { - final camera = Camera( + testWidgets('starts a video recording', (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1045,10 +1070,10 @@ void main() { testWidgets( 'starts a video recording ' - 'with maxVideoDuration', (tester) async { - const maxVideoDuration = Duration(hours: 1); + 'with maxVideoDuration', (WidgetTester tester) async { + const Duration maxVideoDuration = Duration(hours: 1); - final camera = Camera( + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1068,8 +1093,8 @@ void main() { testWidgets( 'with notSupported error ' 'when maxVideoDuration is 0 milliseconds or less', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1084,12 +1109,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.notSupported, ), @@ -1099,11 +1124,11 @@ void main() { testWidgets( 'with notSupported error ' - 'when no video types are supported', (tester) async { - final camera = Camera( + 'when no video types are supported', (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, - )..isVideoTypeSupported = (type) => false; + )..isVideoTypeSupported = (String type) => false; await camera.initialize(); await camera.play(); @@ -1113,12 +1138,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.notSupported, ), @@ -1129,8 +1154,8 @@ void main() { }); group('pauseVideoRecording', () { - testWidgets('pauses a video recording', (tester) async { - final camera = Camera( + testWidgets('pauses a video recording', (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, )..mediaRecorder = mediaRecorder; @@ -1143,8 +1168,9 @@ void main() { testWidgets( 'throws a CameraWebException ' 'with videoRecordingNotStarted error ' - 'if the video recording was not started', (tester) async { - final camera = Camera( + 'if the video recording was not started', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ); @@ -1154,12 +1180,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.videoRecordingNotStarted, ), @@ -1169,8 +1195,8 @@ void main() { }); group('resumeVideoRecording', () { - testWidgets('resumes a video recording', (tester) async { - final camera = Camera( + testWidgets('resumes a video recording', (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, )..mediaRecorder = mediaRecorder; @@ -1183,8 +1209,9 @@ void main() { testWidgets( 'throws a CameraWebException ' 'with videoRecordingNotStarted error ' - 'if the video recording was not started', (tester) async { - final camera = Camera( + 'if the video recording was not started', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ); @@ -1194,12 +1221,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.videoRecordingNotStarted, ), @@ -1212,8 +1239,8 @@ void main() { testWidgets( 'stops a video recording and ' 'returns the captured file ' - 'based on all video data parts', (tester) async { - final camera = Camera( + 'based on all video data parts', (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1228,31 +1255,33 @@ void main() { when( () => mediaRecorder.addEventListener('dataavailable', any()), - ).thenAnswer((invocation) { - videoDataAvailableListener = invocation.positionalArguments[1]; + ).thenAnswer((Invocation invocation) { + videoDataAvailableListener = + invocation.positionalArguments[1] as void Function(Event); }); when( () => mediaRecorder.addEventListener('stop', any()), - ).thenAnswer((invocation) { - videoRecordingStoppedListener = invocation.positionalArguments[1]; + ).thenAnswer((Invocation invocation) { + videoRecordingStoppedListener = + invocation.positionalArguments[1] as void Function(Event); }); Blob? finalVideo; List? videoParts; - camera.blobBuilder = (blobs, videoType) { - videoParts = [...blobs]; + camera.blobBuilder = (List blobs, String videoType) { + videoParts = [...blobs]; finalVideo = Blob(blobs, videoType); return finalVideo!; }; await camera.startVideoRecording(); - final videoFileFuture = camera.stopVideoRecording(); + final Future videoFileFuture = camera.stopVideoRecording(); - final capturedVideoPartOne = Blob([]); - final capturedVideoPartTwo = Blob([]); + final Blob capturedVideoPartOne = Blob([]); + final Blob capturedVideoPartTwo = Blob([]); - final capturedVideoParts = [ + final List capturedVideoParts = [ capturedVideoPartOne, capturedVideoPartTwo, ]; @@ -1263,7 +1292,7 @@ void main() { videoRecordingStoppedListener.call(Event('stop')); - final videoFile = await videoFileFuture; + final XFile videoFile = await videoFileFuture; verify(mediaRecorder.stop).called(1); @@ -1291,8 +1320,9 @@ void main() { testWidgets( 'throws a CameraWebException ' 'with videoRecordingNotStarted error ' - 'if the video recording was not started', (tester) async { - final camera = Camera( + 'if the video recording was not started', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ); @@ -1302,12 +1332,12 @@ void main() { throwsA( isA() .having( - (e) => e.cameraId, + (CameraWebException e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.code, + (CameraWebException e) => e.code, 'code', CameraErrorCode.videoRecordingNotStarted, ), @@ -1322,18 +1352,20 @@ void main() { setUp(() { when( () => mediaRecorder.addEventListener('dataavailable', any()), - ).thenAnswer((invocation) { - videoDataAvailableListener = invocation.positionalArguments[1]; + ).thenAnswer((Invocation invocation) { + videoDataAvailableListener = + invocation.positionalArguments[1] as void Function(Event); }); }); testWidgets( 'stops a video recording ' 'if maxVideoDuration is given and ' - 'the recording was not stopped manually', (tester) async { - const maxVideoDuration = Duration(hours: 1); + 'the recording was not stopped manually', + (WidgetTester tester) async { + const Duration maxVideoDuration = Duration(hours: 1); - final camera = Camera( + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1346,9 +1378,9 @@ void main() { when(() => mediaRecorder.state).thenReturn('recording'); - videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); - await Future.microtask(() {}); + await Future.microtask(() {}); verify(mediaRecorder.stop).called(1); }); @@ -1360,14 +1392,15 @@ void main() { setUp(() { when( () => mediaRecorder.addEventListener('stop', any()), - ).thenAnswer((invocation) { - videoRecordingStoppedListener = invocation.positionalArguments[1]; + ).thenAnswer((Invocation invocation) { + videoRecordingStoppedListener = + invocation.positionalArguments[1] as void Function(Event); }); }); testWidgets('stops listening to the media recorder data events', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1381,7 +1414,7 @@ void main() { videoRecordingStoppedListener.call(Event('stop')); - await Future.microtask(() {}); + await Future.microtask(() {}); verify( () => mediaRecorder.removeEventListener('dataavailable', any()), @@ -1389,8 +1422,8 @@ void main() { }); testWidgets('stops listening to the media recorder stop events', - (tester) async { - final camera = Camera( + (WidgetTester tester) async { + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1404,7 +1437,7 @@ void main() { videoRecordingStoppedListener.call(Event('stop')); - await Future.microtask(() {}); + await Future.microtask(() {}); verify( () => mediaRecorder.removeEventListener('stop', any()), @@ -1412,10 +1445,11 @@ void main() { }); testWidgets('stops listening to the media recorder errors', - (tester) async { - final onErrorStreamController = StreamController(); + (WidgetTester tester) async { + final StreamController onErrorStreamController = + StreamController(); - final camera = Camera( + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) @@ -1432,7 +1466,7 @@ void main() { videoRecordingStoppedListener.call(Event('stop')); - await Future.microtask(() {}); + await Future.microtask(() {}); expect( onErrorStreamController.hasListener, @@ -1443,8 +1477,9 @@ void main() { }); group('dispose', () { - testWidgets('resets the video element\'s source', (tester) async { - final camera = Camera( + testWidgets("resets the video element's source", + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -1455,8 +1490,8 @@ void main() { expect(camera.videoElement.srcObject, isNull); }); - testWidgets('closes the onEnded stream', (tester) async { - final camera = Camera( + testWidgets('closes the onEnded stream', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -1470,8 +1505,9 @@ void main() { ); }); - testWidgets('closes the onVideoRecordedEvent stream', (tester) async { - final camera = Camera( + testWidgets('closes the onVideoRecordedEvent stream', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -1485,8 +1521,9 @@ void main() { ); }); - testWidgets('closes the onVideoRecordingError stream', (tester) async { - final camera = Camera( + testWidgets('closes the onVideoRecordingError stream', + (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); @@ -1505,20 +1542,20 @@ void main() { group('onVideoRecordedEvent', () { testWidgets( 'emits a VideoRecordedEvent ' - 'when a video recording is created', (tester) async { - const maxVideoDuration = Duration(hours: 1); - const supportedVideoType = 'video/webm'; + 'when a video recording is created', (WidgetTester tester) async { + const Duration maxVideoDuration = Duration(hours: 1); + const String supportedVideoType = 'video/webm'; - final mediaRecorder = MockMediaRecorder(); + final MockMediaRecorder mediaRecorder = MockMediaRecorder(); when(() => mediaRecorder.onError) - .thenAnswer((_) => const Stream.empty()); + .thenAnswer((_) => const Stream.empty()); - final camera = Camera( + final Camera camera = Camera( textureId: 1, cameraService: cameraService, ) ..mediaRecorder = mediaRecorder - ..isVideoTypeSupported = (type) => type == 'video/webm'; + ..isVideoTypeSupported = (String type) => type == 'video/webm'; await camera.initialize(); await camera.play(); @@ -1528,27 +1565,30 @@ void main() { when( () => mediaRecorder.addEventListener('dataavailable', any()), - ).thenAnswer((invocation) { - videoDataAvailableListener = invocation.positionalArguments[1]; + ).thenAnswer((Invocation invocation) { + videoDataAvailableListener = + invocation.positionalArguments[1] as void Function(Event); }); when( () => mediaRecorder.addEventListener('stop', any()), - ).thenAnswer((invocation) { - videoRecordingStoppedListener = invocation.positionalArguments[1]; + ).thenAnswer((Invocation invocation) { + videoRecordingStoppedListener = + invocation.positionalArguments[1] as void Function(Event); }); - final streamQueue = StreamQueue(camera.onVideoRecordedEvent); + final StreamQueue streamQueue = + StreamQueue(camera.onVideoRecordedEvent); await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); Blob? finalVideo; - camera.blobBuilder = (blobs, videoType) { + camera.blobBuilder = (List blobs, String videoType) { finalVideo = Blob(blobs, videoType); return finalVideo!; }; - videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); videoRecordingStoppedListener.call(Event('stop')); expect( @@ -1556,27 +1596,27 @@ void main() { equals( isA() .having( - (e) => e.cameraId, + (VideoRecordedEvent e) => e.cameraId, 'cameraId', textureId, ) .having( - (e) => e.file, + (VideoRecordedEvent e) => e.file, 'file', isA() .having( - (f) => f.mimeType, + (XFile f) => f.mimeType, 'mimeType', supportedVideoType, ) .having( - (f) => f.name, + (XFile f) => f.name, 'name', finalVideo.hashCode.toString(), ), ) .having( - (e) => e.maxVideoDuration, + (VideoRecordedEvent e) => e.maxVideoDuration, 'maxVideoDuration', maxVideoDuration, ), @@ -1590,18 +1630,20 @@ void main() { group('onEnded', () { testWidgets( 'emits the default video track ' - 'when it emits an ended event', (tester) async { - final camera = Camera( + 'when it emits an ended event', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final streamQueue = StreamQueue(camera.onEnded); + final StreamQueue streamQueue = + StreamQueue(camera.onEnded); await camera.initialize(); - final videoTracks = camera.stream!.getVideoTracks(); - final defaultVideoTrack = videoTracks.first; + final List videoTracks = + camera.stream!.getVideoTracks(); + final MediaStreamTrack defaultVideoTrack = videoTracks.first; defaultVideoTrack.dispatchEvent(Event('ended')); @@ -1615,18 +1657,20 @@ void main() { testWidgets( 'emits the default video track ' - 'when the camera is stopped', (tester) async { - final camera = Camera( + 'when the camera is stopped', (WidgetTester tester) async { + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, ); - final streamQueue = StreamQueue(camera.onEnded); + final StreamQueue streamQueue = + StreamQueue(camera.onEnded); await camera.initialize(); - final videoTracks = camera.stream!.getVideoTracks(); - final defaultVideoTrack = videoTracks.first; + final List videoTracks = + camera.stream!.getVideoTracks(); + final MediaStreamTrack defaultVideoTrack = videoTracks.first; camera.stop(); @@ -1643,11 +1687,12 @@ void main() { testWidgets( 'emits an ErrorEvent ' 'when the media recorder fails ' - 'when recording a video', (tester) async { - final mediaRecorder = MockMediaRecorder(); - final errorController = StreamController(); + 'when recording a video', (WidgetTester tester) async { + final MockMediaRecorder mediaRecorder = MockMediaRecorder(); + final StreamController errorController = + StreamController(); - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: cameraService, )..mediaRecorder = mediaRecorder; @@ -1655,14 +1700,15 @@ void main() { when(() => mediaRecorder.onError) .thenAnswer((_) => errorController.stream); - final streamQueue = StreamQueue(camera.onVideoRecordingError); + final StreamQueue streamQueue = + StreamQueue(camera.onVideoRecordingError); await camera.initialize(); await camera.play(); await camera.startVideoRecording(); - final errorEvent = ErrorEvent('type'); + final ErrorEvent errorEvent = ErrorEvent('type'); errorController.add(errorEvent); expect( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart index 6f8531b6f4af..fcb54da1aed5 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart @@ -10,24 +10,27 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CameraWebException', () { - testWidgets('sets all properties', (tester) async { - final cameraId = 1; - final code = CameraErrorCode.notFound; - final description = 'The camera is not found.'; + testWidgets('sets all properties', (WidgetTester tester) async { + const int cameraId = 1; + const CameraErrorCode code = CameraErrorCode.notFound; + const String description = 'The camera is not found.'; - final exception = CameraWebException(cameraId, code, description); + final CameraWebException exception = + CameraWebException(cameraId, code, description); expect(exception.cameraId, equals(cameraId)); expect(exception.code, equals(code)); expect(exception.description, equals(description)); }); - testWidgets('toString includes all properties', (tester) async { - final cameraId = 2; - final code = CameraErrorCode.notReadable; - final description = 'The camera is not readable.'; + testWidgets('toString includes all properties', + (WidgetTester tester) async { + const int cameraId = 2; + const CameraErrorCode code = CameraErrorCode.notReadable; + const String description = 'The camera is not readable.'; - final exception = CameraWebException(cameraId, code, description); + final CameraWebException exception = + CameraWebException(cameraId, code, description); expect( exception.toString(), 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 9749559ed8c6..7fe6266587ae 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 cameraId = 0; + const int cameraId = 0; late Window window; late Navigator navigator; @@ -42,7 +44,7 @@ void main() { navigator = MockNavigator(); mediaDevices = MockMediaDevices(); - videoElement = getVideoElementWithBlankStream(Size(10, 10)); + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); @@ -76,12 +78,13 @@ void main() { }); setUpAll(() { - registerFallbackValue(MockMediaStreamTrack()); - registerFallbackValue(MockCameraOptions()); - registerFallbackValue(FlashMode.off); + registerFallbackValue(MockMediaStreamTrack()); + registerFallbackValue(MockCameraOptions()); + registerFallbackValue(FlashMode.off); }); - testWidgets('CameraPlugin is the live instance', (tester) async { + testWidgets('CameraPlugin is the live instance', + (WidgetTester tester) async { expect(CameraPlatform.instance, isA()); }); @@ -94,16 +97,18 @@ void main() { ).thenReturn(null); when(mediaDevices.enumerateDevices).thenAnswer( - (_) async => [], + (_) async => [], ); }); - testWidgets('requests video and audio permissions', (tester) async { - final _ = await CameraPlatform.instance.availableCameras(); + testWidgets('requests video and audio permissions', + (WidgetTester tester) async { + final List _ = + await CameraPlatform.instance.availableCameras(); verify( () => cameraService.getMediaStreamForOptions( - CameraOptions( + const CameraOptions( audio: AudioConstraints(enabled: true), ), ), @@ -112,45 +117,48 @@ void main() { testWidgets( 'releases the camera stream ' - 'used to request video and audio permissions', (tester) async { - final videoTrack = MockMediaStreamTrack(); + 'used to request video and audio permissions', + (WidgetTester tester) async { + final MockMediaStreamTrack videoTrack = MockMediaStreamTrack(); - var videoTrackStopped = false; - when(videoTrack.stop).thenAnswer((_) { + bool videoTrackStopped = false; + when(videoTrack.stop).thenAnswer((Invocation _) { videoTrackStopped = true; }); when( () => cameraService.getMediaStreamForOptions( - CameraOptions( + const CameraOptions( audio: AudioConstraints(enabled: true), ), ), ).thenAnswer( - (_) => Future.value( - FakeMediaStream([videoTrack]), + (_) => Future.value( + FakeMediaStream([videoTrack]), ), ); - final _ = await CameraPlatform.instance.availableCameras(); + final List _ = + await CameraPlatform.instance.availableCameras(); expect(videoTrackStopped, isTrue); }); testWidgets( 'gets a video stream ' - 'for a video input device', (tester) async { - final videoDevice = FakeMediaDeviceInfo( + 'for a video input device', (WidgetTester tester) async { + final FakeMediaDeviceInfo videoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', MediaDeviceKind.videoInput, ); when(mediaDevices.enumerateDevices).thenAnswer( - (_) => Future.value([videoDevice]), + (_) => Future>.value([videoDevice]), ); - final _ = await CameraPlatform.instance.availableCameras(); + final List _ = + await CameraPlatform.instance.availableCameras(); verify( () => cameraService.getMediaStreamForOptions( @@ -166,18 +174,19 @@ void main() { testWidgets( 'does not get a video stream ' 'for the video input device ' - 'with an empty device id', (tester) async { - final videoDevice = FakeMediaDeviceInfo( + 'with an empty device id', (WidgetTester tester) async { + final FakeMediaDeviceInfo videoDevice = FakeMediaDeviceInfo( '', 'Camera 1', MediaDeviceKind.videoInput, ); when(mediaDevices.enumerateDevices).thenAnswer( - (_) => Future.value([videoDevice]), + (_) => Future>.value([videoDevice]), ); - final _ = await CameraPlatform.instance.availableCameras(); + final List _ = + await CameraPlatform.instance.availableCameras(); verifyNever( () => cameraService.getMediaStreamForOptions( @@ -193,15 +202,15 @@ void main() { testWidgets( 'gets the facing mode ' 'from the first available video track ' - 'of the video input device', (tester) async { - final videoDevice = FakeMediaDeviceInfo( + 'of the video input device', (WidgetTester tester) async { + final FakeMediaDeviceInfo videoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', MediaDeviceKind.videoInput, ); - final videoStream = - FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + final FakeMediaStream videoStream = FakeMediaStream( + [MockMediaStreamTrack(), MockMediaStreamTrack()]); when( () => cameraService.getMediaStreamForOptions( @@ -209,13 +218,14 @@ void main() { video: VideoConstraints(deviceId: videoDevice.deviceId), ), ), - ).thenAnswer((_) => Future.value(videoStream)); + ).thenAnswer((Invocation _) => Future.value(videoStream)); when(mediaDevices.enumerateDevices).thenAnswer( - (_) => Future.value([videoDevice]), + (_) => Future>.value([videoDevice]), ); - final _ = await CameraPlatform.instance.availableCameras(); + final List _ = + await CameraPlatform.instance.availableCameras(); verify( () => cameraService.getFacingModeForVideoTrack( @@ -227,30 +237,31 @@ void main() { testWidgets( 'returns appropriate camera descriptions ' 'for multiple video devices ' - 'based on video streams', (tester) async { - final firstVideoDevice = FakeMediaDeviceInfo( + 'based on video streams', (WidgetTester tester) async { + final FakeMediaDeviceInfo firstVideoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', MediaDeviceKind.videoInput, ); - final secondVideoDevice = FakeMediaDeviceInfo( + final FakeMediaDeviceInfo secondVideoDevice = FakeMediaDeviceInfo( '4', 'Camera 4', MediaDeviceKind.videoInput, ); // Create a video stream for the first video device. - final firstVideoStream = - FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + final FakeMediaStream firstVideoStream = FakeMediaStream( + [MockMediaStreamTrack(), MockMediaStreamTrack()]); // Create a video stream for the second video device. - final secondVideoStream = FakeMediaStream([MockMediaStreamTrack()]); + final FakeMediaStream secondVideoStream = + FakeMediaStream([MockMediaStreamTrack()]); // Mock media devices to return two video input devices // and two audio devices. when(mediaDevices.enumerateDevices).thenAnswer( - (_) => Future.value([ + (_) => Future>.value([ firstVideoDevice, FakeMediaDeviceInfo( '2', @@ -274,7 +285,8 @@ void main() { video: VideoConstraints(deviceId: firstVideoDevice.deviceId), ), ), - ).thenAnswer((_) => Future.value(firstVideoStream)); + ).thenAnswer( + (Invocation _) => Future.value(firstVideoStream)); // Mock camera service to return the second video stream // for the second video device. @@ -284,7 +296,8 @@ void main() { video: VideoConstraints(deviceId: secondVideoDevice.deviceId), ), ), - ).thenAnswer((_) => Future.value(secondVideoStream)); + ).thenAnswer( + (Invocation _) => Future.value(secondVideoStream)); // Mock camera service to return a user facing mode // for the first video stream. @@ -308,12 +321,13 @@ void main() { when(() => cameraService.mapFacingModeToLensDirection('environment')) .thenReturn(CameraLensDirection.back); - final cameras = await CameraPlatform.instance.availableCameras(); + final List cameras = + await CameraPlatform.instance.availableCameras(); // Expect two cameras and ignore two audio devices. expect( cameras, - equals([ + equals([ CameraDescription( name: firstVideoDevice.label!, lensDirection: CameraLensDirection.front, @@ -330,18 +344,18 @@ void main() { testWidgets( 'sets camera metadata ' - 'for the camera description', (tester) async { - final videoDevice = FakeMediaDeviceInfo( + 'for the camera description', (WidgetTester tester) async { + final FakeMediaDeviceInfo videoDevice = FakeMediaDeviceInfo( '1', 'Camera 1', MediaDeviceKind.videoInput, ); - final videoStream = - FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + final FakeMediaStream videoStream = FakeMediaStream( + [MockMediaStreamTrack(), MockMediaStreamTrack()]); when(mediaDevices.enumerateDevices).thenAnswer( - (_) => Future.value([videoDevice]), + (_) => Future>.value([videoDevice]), ); when( @@ -350,7 +364,7 @@ void main() { video: VideoConstraints(deviceId: videoDevice.deviceId), ), ), - ).thenAnswer((_) => Future.value(videoStream)); + ).thenAnswer((Invocation _) => Future.value(videoStream)); when( () => cameraService.getFacingModeForVideoTrack( @@ -361,11 +375,12 @@ void main() { when(() => cameraService.mapFacingModeToLensDirection('left')) .thenReturn(CameraLensDirection.external); - final camera = (await CameraPlatform.instance.availableCameras()).first; + final CameraDescription camera = + (await CameraPlatform.instance.availableCameras()).first; expect( (CameraPlatform.instance as CameraPlugin).camerasMetadata, - equals({ + equals({ camera: CameraMetadata( deviceId: videoDevice.deviceId!, facingMode: 'left', @@ -374,17 +389,50 @@ void main() { ); }); + testWidgets( + 'releases the video stream ' + 'of a video input device', (WidgetTester tester) async { + final FakeMediaDeviceInfo videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final FakeMediaStream videoStream = FakeMediaStream( + [MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future>.value([videoDevice]), + ); + + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ), + ), + ).thenAnswer((Invocation _) => Future.value(videoStream)); + + final List _ = + await CameraPlatform.instance.availableCameras(); + + for (final MediaStreamTrack videoTrack + in videoStream.getVideoTracks()) { + verify(videoTrack.stop).called(1); + } + }); + group('throws CameraException', () { testWidgets( 'with notSupported error ' - 'when there are no media devices', (tester) async { + 'when there are no media devices', (WidgetTester tester) async { when(() => navigator.mediaDevices).thenReturn(null); expect( () => CameraPlatform.instance.availableCameras(), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', CameraErrorCode.notSupported.toString(), ), @@ -393,8 +441,9 @@ void main() { }); testWidgets('when MediaDevices.enumerateDevices throws DomException', - (tester) async { - final exception = FakeDomException(DomException.UNKNOWN); + (WidgetTester tester) async { + final FakeDomException exception = + FakeDomException(DomException.UNKNOWN); when(mediaDevices.enumerateDevices).thenThrow(exception); @@ -402,7 +451,7 @@ void main() { () => CameraPlatform.instance.availableCameras(), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', exception.name, ), @@ -412,8 +461,8 @@ void main() { testWidgets( 'when CameraService.getMediaStreamForOptions ' - 'throws CameraWebException', (tester) async { - final exception = CameraWebException( + 'throws CameraWebException', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.security, 'description', @@ -426,7 +475,7 @@ void main() { () => CameraPlatform.instance.availableCameras(), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', exception.code.toString(), ), @@ -436,8 +485,8 @@ void main() { testWidgets( 'when CameraService.getMediaStreamForOptions ' - 'throws PlatformException', (tester) async { - final exception = PlatformException( + 'throws PlatformException', (WidgetTester tester) async { + final PlatformException exception = PlatformException( code: CameraErrorCode.notSupported.toString(), message: 'message', ); @@ -449,9 +498,9 @@ void main() { () => CameraPlatform.instance.availableCameras(), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', - exception.code.toString(), + exception.code, ), ), ); @@ -461,16 +510,16 @@ void main() { group('createCamera', () { group('creates a camera', () { - const ultraHighResolutionSize = Size(3840, 2160); - const maxResolutionSize = Size(3840, 2160); + const Size ultraHighResolutionSize = Size(3840, 2160); + const Size maxResolutionSize = Size(3840, 2160); - final cameraDescription = CameraDescription( + const CameraDescription cameraDescription = CameraDescription( name: 'name', lensDirection: CameraLensDirection.front, sensorOrientation: 0, ); - final cameraMetadata = CameraMetadata( + const CameraMetadata cameraMetadata = CameraMetadata( deviceId: 'deviceId', facingMode: 'user', ); @@ -485,13 +534,13 @@ void main() { ).thenReturn(CameraType.user); }); - testWidgets('with appropriate options', (tester) async { + testWidgets('with appropriate options', (WidgetTester tester) async { when( () => cameraService .mapResolutionPresetToSize(ResolutionPreset.ultraHigh), ).thenReturn(ultraHighResolutionSize); - final cameraId = await CameraPlatform.instance.createCamera( + final int cameraId = await CameraPlatform.instance.createCamera( cameraDescription, ResolutionPreset.ultraHigh, enableAudio: true, @@ -501,15 +550,15 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId], isA() .having( - (camera) => camera.textureId, + (Camera camera) => camera.textureId, 'textureId', cameraId, ) .having( - (camera) => camera.options, + (Camera camera) => camera.options, 'options', CameraOptions( - audio: AudioConstraints(enabled: true), + audio: const AudioConstraints(enabled: true), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.user), width: VideoSizeConstraint( @@ -528,12 +577,12 @@ void main() { testWidgets( 'with a max resolution preset ' 'and enabled audio set to false ' - 'when no options are specified', (tester) async { + 'when no options are specified', (WidgetTester tester) async { when( () => cameraService.mapResolutionPresetToSize(ResolutionPreset.max), ).thenReturn(maxResolutionSize); - final cameraId = await CameraPlatform.instance.createCamera( + final int cameraId = await CameraPlatform.instance.createCamera( cameraDescription, null, ); @@ -541,10 +590,10 @@ void main() { expect( (CameraPlatform.instance as CameraPlugin).cameras[cameraId], isA().having( - (camera) => camera.options, + (Camera camera) => camera.options, 'options', CameraOptions( - audio: AudioConstraints(enabled: false), + audio: const AudioConstraints(enabled: false), video: VideoConstraints( facingMode: FacingModeConstraint(CameraType.user), width: VideoSizeConstraint( @@ -565,10 +614,10 @@ void main() { 'throws CameraException ' 'with missingMetadata error ' 'if there is no metadata ' - 'for the given camera description', (tester) async { + 'for the given camera description', (WidgetTester tester) async { expect( () => CameraPlatform.instance.createCamera( - CameraDescription( + const CameraDescription( name: 'name', lensDirection: CameraLensDirection.back, sensorOrientation: 0, @@ -577,7 +626,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', CameraErrorCode.missingMetadata.toString(), ), @@ -601,21 +650,23 @@ void main() { abortStreamController = StreamController(); endedStreamController = StreamController(); - when(camera.getVideoSize).thenReturn(Size(10, 10)); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); + when(camera.getVideoSize).thenReturn(const Size(10, 10)); + when(camera.initialize) + .thenAnswer((Invocation _) => Future.value()); + when(camera.play).thenAnswer((Invocation _) => Future.value()); when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + when(() => videoElement.onError).thenAnswer((Invocation _) => + FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort).thenAnswer((Invocation _) => + FakeElementStream(abortStreamController.stream)); when(() => camera.onEnded) - .thenAnswer((_) => endedStreamController.stream); + .thenAnswer((Invocation _) => endedStreamController.stream); }); - testWidgets('initializes and plays the camera', (tester) async { + testWidgets('initializes and plays the camera', + (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -626,7 +677,7 @@ void main() { }); testWidgets('starts listening to the camera video error and abort events', - (tester) async { + (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -640,7 +691,7 @@ void main() { }); testWidgets('starts listening to the camera ended events', - (tester) async { + (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -654,12 +705,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.initializeCamera(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -667,8 +718,9 @@ void main() { ); }); - testWidgets('when camera throws CameraWebException', (tester) async { - final exception = CameraWebException( + testWidgets('when camera throws CameraWebException', + (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.permissionDenied, 'description', @@ -683,7 +735,7 @@ void main() { () => CameraPlatform.instance.initializeCamera(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -691,10 +743,13 @@ void main() { ); }); - testWidgets('when camera throws DomException', (tester) async { - final exception = FakeDomException(DomException.NOT_ALLOWED); + testWidgets('when camera throws DomException', + (WidgetTester tester) async { + final FakeDomException exception = + FakeDomException(DomException.NOT_ALLOWED); - when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.initialize) + .thenAnswer((Invocation _) => Future.value()); when(camera.play).thenThrow(exception); // Save the camera in the camera plugin. @@ -704,9 +759,9 @@ void main() { () => CameraPlatform.instance.initializeCamera(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', - exception.name.toString(), + exception.name, ), ), ); @@ -723,7 +778,7 @@ void main() { testWidgets( 'requests full-screen mode ' - 'on documentElement', (tester) async { + 'on documentElement', (WidgetTester tester) async { await CameraPlatform.instance.lockCaptureOrientation( cameraId, DeviceOrientation.portraitUp, @@ -734,7 +789,7 @@ void main() { testWidgets( 'locks the capture orientation ' - 'based on the given device orientation', (tester) async { + 'based on the given device orientation', (WidgetTester tester) async { when( () => cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, @@ -762,7 +817,7 @@ void main() { group('throws PlatformException', () { testWidgets( 'with orientationNotSupported error ' - 'when screen is not supported', (tester) async { + 'when screen is not supported', (WidgetTester tester) async { when(() => window.screen).thenReturn(null); expect( @@ -772,7 +827,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.orientationNotSupported.toString(), ), @@ -782,7 +837,8 @@ void main() { testWidgets( 'with orientationNotSupported error ' - 'when screen orientation is not supported', (tester) async { + 'when screen orientation is not supported', + (WidgetTester tester) async { when(() => screen.orientation).thenReturn(null); expect( @@ -792,7 +848,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.orientationNotSupported.toString(), ), @@ -802,7 +858,8 @@ void main() { testWidgets( 'with orientationNotSupported error ' - 'when documentElement is not available', (tester) async { + 'when documentElement is not available', + (WidgetTester tester) async { when(() => document.documentElement).thenReturn(null); expect( @@ -812,7 +869,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.orientationNotSupported.toString(), ), @@ -820,8 +877,10 @@ void main() { ); }); - testWidgets('when lock throws DomException', (tester) async { - final exception = FakeDomException(DomException.NOT_ALLOWED); + testWidgets('when lock throws DomException', + (WidgetTester tester) async { + final FakeDomException exception = + FakeDomException(DomException.NOT_ALLOWED); when(() => screenOrientation.lock(any())).thenThrow(exception); @@ -832,7 +891,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -849,7 +908,8 @@ void main() { ).thenReturn(OrientationType.portraitPrimary); }); - testWidgets('unlocks the capture orientation', (tester) async { + testWidgets('unlocks the capture orientation', + (WidgetTester tester) async { await CameraPlatform.instance.unlockCaptureOrientation( cameraId, ); @@ -860,7 +920,7 @@ void main() { group('throws PlatformException', () { testWidgets( 'with orientationNotSupported error ' - 'when screen is not supported', (tester) async { + 'when screen is not supported', (WidgetTester tester) async { when(() => window.screen).thenReturn(null); expect( @@ -869,7 +929,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.orientationNotSupported.toString(), ), @@ -879,7 +939,8 @@ void main() { testWidgets( 'with orientationNotSupported error ' - 'when screen orientation is not supported', (tester) async { + 'when screen orientation is not supported', + (WidgetTester tester) async { when(() => screen.orientation).thenReturn(null); expect( @@ -888,7 +949,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.orientationNotSupported.toString(), ), @@ -898,7 +959,8 @@ void main() { testWidgets( 'with orientationNotSupported error ' - 'when documentElement is not available', (tester) async { + 'when documentElement is not available', + (WidgetTester tester) async { when(() => document.documentElement).thenReturn(null); expect( @@ -907,7 +969,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.orientationNotSupported.toString(), ), @@ -915,8 +977,10 @@ void main() { ); }); - testWidgets('when unlock throws DomException', (tester) async { - final exception = FakeDomException(DomException.NOT_ALLOWED); + testWidgets('when unlock throws DomException', + (WidgetTester tester) async { + final FakeDomException exception = + FakeDomException(DomException.NOT_ALLOWED); when(screenOrientation.unlock).thenThrow(exception); @@ -926,7 +990,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -937,17 +1001,18 @@ void main() { }); group('takePicture', () { - testWidgets('captures a picture', (tester) async { - final camera = MockCamera(); - final capturedPicture = MockXFile(); + testWidgets('captures a picture', (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final MockXFile capturedPicture = MockXFile(); when(camera.takePicture) - .thenAnswer((_) => Future.value(capturedPicture)); + .thenAnswer((Invocation _) => Future.value(capturedPicture)); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - final picture = await CameraPlatform.instance.takePicture(cameraId); + final XFile picture = + await CameraPlatform.instance.takePicture(cameraId); verify(camera.takePicture).called(1); @@ -957,12 +1022,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.takePicture(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -970,9 +1035,11 @@ void main() { ); }); - testWidgets('when takePicture throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when takePicture throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(camera.takePicture).thenThrow(exception); @@ -983,7 +1050,7 @@ void main() { () => CameraPlatform.instance.takePicture(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -992,9 +1059,9 @@ void main() { }); testWidgets('when takePicture throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1009,7 +1076,7 @@ void main() { () => CameraPlatform.instance.takePicture(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1025,13 +1092,13 @@ void main() { setUp(() { camera = MockCamera(); - when(camera.startVideoRecording).thenAnswer((_) async {}); + when(camera.startVideoRecording).thenAnswer((Invocation _) async {}); when(() => camera.onVideoRecordingError) - .thenAnswer((_) => const Stream.empty()); + .thenAnswer((Invocation _) => const Stream.empty()); }); - testWidgets('starts a video recording', (tester) async { + testWidgets('starts a video recording', (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1041,11 +1108,12 @@ void main() { }); testWidgets('listens to the onVideoRecordingError stream', - (tester) async { - final videoRecordingErrorController = StreamController(); + (WidgetTester tester) async { + final StreamController videoRecordingErrorController = + StreamController(); when(() => camera.onVideoRecordingError) - .thenAnswer((_) => videoRecordingErrorController.stream); + .thenAnswer((Invocation _) => videoRecordingErrorController.stream); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1061,12 +1129,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.startVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1075,8 +1143,9 @@ void main() { }); testWidgets('when startVideoRecording throws DomException', - (tester) async { - final exception = FakeDomException(DomException.INVALID_STATE); + (WidgetTester tester) async { + final FakeDomException exception = + FakeDomException(DomException.INVALID_STATE); when(camera.startVideoRecording).thenThrow(exception); @@ -1087,7 +1156,7 @@ void main() { () => CameraPlatform.instance.startVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1096,8 +1165,8 @@ void main() { }); testWidgets('when startVideoRecording throws CameraWebException', - (tester) async { - final exception = CameraWebException( + (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1112,7 +1181,7 @@ void main() { () => CameraPlatform.instance.startVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1123,17 +1192,17 @@ void main() { }); group('stopVideoRecording', () { - testWidgets('stops a video recording', (tester) async { - final camera = MockCamera(); - final capturedVideo = MockXFile(); + testWidgets('stops a video recording', (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final MockXFile capturedVideo = MockXFile(); when(camera.stopVideoRecording) - .thenAnswer((_) => Future.value(capturedVideo)); + .thenAnswer((Invocation _) => Future.value(capturedVideo)); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - final video = + final XFile video = await CameraPlatform.instance.stopVideoRecording(cameraId); verify(camera.stopVideoRecording).called(1); @@ -1142,23 +1211,25 @@ void main() { }); testWidgets('stops listening to the onVideoRecordingError stream', - (tester) async { - final camera = MockCamera(); - final videoRecordingErrorController = StreamController(); + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final StreamController videoRecordingErrorController = + StreamController(); - when(camera.startVideoRecording).thenAnswer((_) async => {}); + when(camera.startVideoRecording).thenAnswer((Invocation _) async {}); when(camera.stopVideoRecording) - .thenAnswer((_) => Future.value(MockXFile())); + .thenAnswer((Invocation _) => Future.value(MockXFile())); when(() => camera.onVideoRecordingError) - .thenAnswer((_) => videoRecordingErrorController.stream); + .thenAnswer((Invocation _) => videoRecordingErrorController.stream); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; await CameraPlatform.instance.startVideoRecording(cameraId); - final _ = await CameraPlatform.instance.stopVideoRecording(cameraId); + final XFile _ = + await CameraPlatform.instance.stopVideoRecording(cameraId); expect( videoRecordingErrorController.hasListener, @@ -1169,12 +1240,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.stopVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1183,9 +1254,10 @@ void main() { }); testWidgets('when stopVideoRecording throws DomException', - (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.INVALID_STATE); + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.INVALID_STATE); when(camera.stopVideoRecording).thenThrow(exception); @@ -1196,7 +1268,7 @@ void main() { () => CameraPlatform.instance.stopVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1205,9 +1277,9 @@ void main() { }); testWidgets('when stopVideoRecording throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1222,7 +1294,7 @@ void main() { () => CameraPlatform.instance.stopVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1233,10 +1305,10 @@ void main() { }); group('pauseVideoRecording', () { - testWidgets('pauses a video recording', (tester) async { - final camera = MockCamera(); + testWidgets('pauses a video recording', (WidgetTester tester) async { + final MockCamera camera = MockCamera(); - when(camera.pauseVideoRecording).thenAnswer((_) async {}); + when(camera.pauseVideoRecording).thenAnswer((Invocation _) async {}); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1249,12 +1321,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.pauseVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1263,9 +1335,10 @@ void main() { }); testWidgets('when pauseVideoRecording throws DomException', - (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.INVALID_STATE); + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.INVALID_STATE); when(camera.pauseVideoRecording).thenThrow(exception); @@ -1276,7 +1349,7 @@ void main() { () => CameraPlatform.instance.pauseVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1285,9 +1358,9 @@ void main() { }); testWidgets('when pauseVideoRecording throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1302,7 +1375,7 @@ void main() { () => CameraPlatform.instance.pauseVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1313,10 +1386,10 @@ void main() { }); group('resumeVideoRecording', () { - testWidgets('resumes a video recording', (tester) async { - final camera = MockCamera(); + testWidgets('resumes a video recording', (WidgetTester tester) async { + final MockCamera camera = MockCamera(); - when(camera.resumeVideoRecording).thenAnswer((_) async {}); + when(camera.resumeVideoRecording).thenAnswer((Invocation _) async {}); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1329,12 +1402,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.resumeVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1343,9 +1416,10 @@ void main() { }); testWidgets('when resumeVideoRecording throws DomException', - (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.INVALID_STATE); + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.INVALID_STATE); when(camera.resumeVideoRecording).thenThrow(exception); @@ -1356,7 +1430,7 @@ void main() { () => CameraPlatform.instance.resumeVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1365,9 +1439,9 @@ void main() { }); testWidgets('when resumeVideoRecording throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1382,7 +1456,7 @@ void main() { () => CameraPlatform.instance.resumeVideoRecording(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1393,9 +1467,10 @@ void main() { }); group('setFlashMode', () { - testWidgets('calls setFlashMode on the camera', (tester) async { - final camera = MockCamera(); - const flashMode = FlashMode.always; + testWidgets('calls setFlashMode on the camera', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + const FlashMode flashMode = FlashMode.always; // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1411,7 +1486,7 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.setFlashMode( cameraId, @@ -1419,7 +1494,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1427,9 +1502,11 @@ void main() { ); }); - testWidgets('when setFlashMode throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when setFlashMode throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(() => camera.setFlashMode(any())).thenThrow(exception); @@ -1443,7 +1520,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1452,9 +1529,9 @@ void main() { }); testWidgets('when setFlashMode throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1472,7 +1549,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1482,7 +1559,8 @@ void main() { }); }); - testWidgets('setExposureMode throws UnimplementedError', (tester) async { + testWidgets('setExposureMode throws UnimplementedError', + (WidgetTester tester) async { expect( () => CameraPlatform.instance.setExposureMode( cameraId, @@ -1492,18 +1570,19 @@ void main() { ); }); - testWidgets('setExposurePoint throws UnimplementedError', (tester) async { + testWidgets('setExposurePoint throws UnimplementedError', + (WidgetTester tester) async { expect( () => CameraPlatform.instance.setExposurePoint( cameraId, - const Point(0, 0), + const Point(0, 0), ), throwsUnimplementedError, ); }); testWidgets('getMinExposureOffset throws UnimplementedError', - (tester) async { + (WidgetTester tester) async { expect( () => CameraPlatform.instance.getMinExposureOffset(cameraId), throwsUnimplementedError, @@ -1511,7 +1590,7 @@ void main() { }); testWidgets('getMaxExposureOffset throws UnimplementedError', - (tester) async { + (WidgetTester tester) async { expect( () => CameraPlatform.instance.getMaxExposureOffset(cameraId), throwsUnimplementedError, @@ -1519,14 +1598,15 @@ void main() { }); testWidgets('getExposureOffsetStepSize throws UnimplementedError', - (tester) async { + (WidgetTester tester) async { expect( () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), throwsUnimplementedError, ); }); - testWidgets('setExposureOffset throws UnimplementedError', (tester) async { + testWidgets('setExposureOffset throws UnimplementedError', + (WidgetTester tester) async { expect( () => CameraPlatform.instance.setExposureOffset( cameraId, @@ -1536,7 +1616,8 @@ void main() { ); }); - testWidgets('setFocusMode throws UnimplementedError', (tester) async { + testWidgets('setFocusMode throws UnimplementedError', + (WidgetTester tester) async { expect( () => CameraPlatform.instance.setFocusMode( cameraId, @@ -1546,20 +1627,22 @@ void main() { ); }); - testWidgets('setFocusPoint throws UnimplementedError', (tester) async { + testWidgets('setFocusPoint throws UnimplementedError', + (WidgetTester tester) async { expect( () => CameraPlatform.instance.setFocusPoint( cameraId, - const Point(0, 0), + const Point(0, 0), ), throwsUnimplementedError, ); }); group('getMaxZoomLevel', () { - testWidgets('calls getMaxZoomLevel on the camera', (tester) async { - final camera = MockCamera(); - const maximumZoomLevel = 100.0; + testWidgets('calls getMaxZoomLevel on the camera', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + const double maximumZoomLevel = 100.0; when(camera.getMaxZoomLevel).thenReturn(maximumZoomLevel); @@ -1579,14 +1662,14 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () async => await CameraPlatform.instance.getMaxZoomLevel( cameraId, ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1594,9 +1677,11 @@ void main() { ); }); - testWidgets('when getMaxZoomLevel throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when getMaxZoomLevel throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(camera.getMaxZoomLevel).thenThrow(exception); @@ -1609,7 +1694,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1618,9 +1703,9 @@ void main() { }); testWidgets('when getMaxZoomLevel throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1637,7 +1722,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1648,9 +1733,10 @@ void main() { }); group('getMinZoomLevel', () { - testWidgets('calls getMinZoomLevel on the camera', (tester) async { - final camera = MockCamera(); - const minimumZoomLevel = 100.0; + testWidgets('calls getMinZoomLevel on the camera', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + const double minimumZoomLevel = 100.0; when(camera.getMinZoomLevel).thenReturn(minimumZoomLevel); @@ -1670,14 +1756,14 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () async => await CameraPlatform.instance.getMinZoomLevel( cameraId, ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1685,9 +1771,11 @@ void main() { ); }); - testWidgets('when getMinZoomLevel throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when getMinZoomLevel throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(camera.getMinZoomLevel).thenThrow(exception); @@ -1700,7 +1788,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1709,9 +1797,9 @@ void main() { }); testWidgets('when getMinZoomLevel throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1728,7 +1816,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1739,13 +1827,14 @@ void main() { }); group('setZoomLevel', () { - testWidgets('calls setZoomLevel on the camera', (tester) async { - final camera = MockCamera(); + testWidgets('calls setZoomLevel on the camera', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - const zoom = 100.0; + const double zoom = 100.0; await CameraPlatform.instance.setZoomLevel(cameraId, zoom); @@ -1755,7 +1844,7 @@ void main() { group('throws CameraException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () async => await CameraPlatform.instance.setZoomLevel( cameraId, @@ -1763,7 +1852,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1771,9 +1860,11 @@ void main() { ); }); - testWidgets('when setZoomLevel throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when setZoomLevel throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(() => camera.setZoomLevel(any())).thenThrow(exception); @@ -1787,7 +1878,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', exception.name, ), @@ -1796,9 +1887,9 @@ void main() { }); testWidgets('when setZoomLevel throws PlatformException', - (tester) async { - final camera = MockCamera(); - final exception = PlatformException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final PlatformException exception = PlatformException( code: CameraErrorCode.notSupported.toString(), message: 'message', ); @@ -1815,7 +1906,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', exception.code, ), @@ -1824,9 +1915,9 @@ void main() { }); testWidgets('when setZoomLevel throws CameraWebException', - (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -1844,7 +1935,7 @@ void main() { ), throwsA( isA().having( - (e) => e.code, + (CameraException e) => e.code, 'code', exception.code.toString(), ), @@ -1855,8 +1946,8 @@ void main() { }); group('pausePreview', () { - testWidgets('calls pause on the camera', (tester) async { - final camera = MockCamera(); + testWidgets('calls pause on the camera', (WidgetTester tester) async { + final MockCamera camera = MockCamera(); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1869,12 +1960,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () async => await CameraPlatform.instance.pausePreview(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1882,9 +1973,11 @@ void main() { ); }); - testWidgets('when pause throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when pause throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(camera.pause).thenThrow(exception); @@ -1895,7 +1988,7 @@ void main() { () async => await CameraPlatform.instance.pausePreview(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1906,10 +1999,10 @@ void main() { }); group('resumePreview', () { - testWidgets('calls play on the camera', (tester) async { - final camera = MockCamera(); + testWidgets('calls play on the camera', (WidgetTester tester) async { + final MockCamera camera = MockCamera(); - when(camera.play).thenAnswer((_) async => {}); + when(camera.play).thenAnswer((Invocation _) async {}); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -1922,12 +2015,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () async => await CameraPlatform.instance.resumePreview(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -1935,9 +2028,11 @@ void main() { ); }); - testWidgets('when play throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.NOT_SUPPORTED); + testWidgets('when play throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.NOT_SUPPORTED); when(camera.play).thenThrow(exception); @@ -1948,7 +2043,7 @@ void main() { () async => await CameraPlatform.instance.resumePreview(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -1956,9 +2051,10 @@ void main() { ); }); - testWidgets('when play throws CameraWebException', (tester) async { - final camera = MockCamera(); - final exception = CameraWebException( + testWidgets('when play throws CameraWebException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.unknown, 'description', @@ -1973,7 +2069,7 @@ void main() { () async => await CameraPlatform.instance.resumePreview(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.code.toString(), ), @@ -1985,8 +2081,8 @@ void main() { testWidgets( 'buildPreview returns an HtmlElementView ' - 'with an appropriate view type', (tester) async { - final camera = Camera( + 'with an appropriate view type', (WidgetTester tester) async { + final Camera camera = Camera( textureId: cameraId, cameraService: cameraService, ); @@ -1997,7 +2093,7 @@ void main() { expect( CameraPlatform.instance.buildPreview(cameraId), isA().having( - (view) => view.viewType, + (widgets.HtmlElementView view) => view.viewType, 'viewType', camera.getViewType(), ), @@ -2021,38 +2117,41 @@ void main() { endedStreamController = StreamController(); videoRecordingErrorController = StreamController(); - when(camera.getVideoSize).thenReturn(Size(10, 10)); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); - when(camera.dispose).thenAnswer((_) => Future.value()); + when(camera.getVideoSize).thenReturn(const Size(10, 10)); + when(camera.initialize) + .thenAnswer((Invocation _) => Future.value()); + when(camera.play).thenAnswer((Invocation _) => Future.value()); + when(camera.dispose).thenAnswer((Invocation _) => Future.value()); when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + when(() => videoElement.onError).thenAnswer((Invocation _) => + FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort).thenAnswer((Invocation _) => + FakeElementStream(abortStreamController.stream)); when(() => camera.onEnded) - .thenAnswer((_) => endedStreamController.stream); + .thenAnswer((Invocation _) => endedStreamController.stream); when(() => camera.onVideoRecordingError) - .thenAnswer((_) => videoRecordingErrorController.stream); + .thenAnswer((Invocation _) => videoRecordingErrorController.stream); - when(camera.startVideoRecording).thenAnswer((_) async {}); + when(camera.startVideoRecording).thenAnswer((Invocation _) async {}); }); - testWidgets('disposes the correct camera', (tester) async { - const firstCameraId = 0; - const secondCameraId = 1; + testWidgets('disposes the correct camera', (WidgetTester tester) async { + const int firstCameraId = 0; + const int secondCameraId = 1; - final firstCamera = MockCamera(); - final secondCamera = MockCamera(); + final MockCamera firstCamera = MockCamera(); + final MockCamera secondCamera = MockCamera(); - when(firstCamera.dispose).thenAnswer((_) => Future.value()); - when(secondCamera.dispose).thenAnswer((_) => Future.value()); + when(firstCamera.dispose) + .thenAnswer((Invocation _) => Future.value()); + when(secondCamera.dispose) + .thenAnswer((Invocation _) => Future.value()); // Save cameras in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras.addAll({ + (CameraPlatform.instance as CameraPlugin).cameras.addAll({ firstCameraId: firstCamera, secondCameraId: secondCamera, }); @@ -2067,14 +2166,14 @@ void main() { // The first camera should be removed from the camera plugin. expect( (CameraPlatform.instance as CameraPlugin).cameras, - equals({ + equals({ secondCameraId: secondCamera, }), ); }); testWidgets('cancels the camera video error and abort subscriptions', - (tester) async { + (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -2085,7 +2184,8 @@ void main() { expect(abortStreamController.hasListener, isFalse); }); - testWidgets('cancels the camera ended subscriptions', (tester) async { + testWidgets('cancels the camera ended subscriptions', + (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -2096,7 +2196,7 @@ void main() { }); testWidgets('cancels the camera video recording error subscriptions', - (tester) async { + (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -2110,12 +2210,12 @@ void main() { group('throws PlatformException', () { testWidgets( 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => CameraPlatform.instance.dispose(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -2123,9 +2223,11 @@ void main() { ); }); - testWidgets('when dispose throws DomException', (tester) async { - final camera = MockCamera(); - final exception = FakeDomException(DomException.INVALID_ACCESS); + testWidgets('when dispose throws DomException', + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final FakeDomException exception = + FakeDomException(DomException.INVALID_ACCESS); when(camera.dispose).thenThrow(exception); @@ -2136,7 +2238,7 @@ void main() { () => CameraPlatform.instance.dispose(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', exception.name, ), @@ -2147,8 +2249,8 @@ void main() { }); group('getCamera', () { - testWidgets('returns the correct camera', (tester) async { - final camera = Camera( + testWidgets('returns the correct camera', (WidgetTester tester) async { + final Camera camera = Camera( textureId: cameraId, cameraService: cameraService, ); @@ -2165,12 +2267,12 @@ void main() { testWidgets( 'throws PlatformException ' 'with notFound error ' - 'if the camera does not exist', (tester) async { + 'if the camera does not exist', (WidgetTester tester) async { expect( () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), throwsA( isA().having( - (e) => e.code, + (PlatformException e) => e.code, 'code', CameraErrorCode.notFound.toString(), ), @@ -2196,30 +2298,32 @@ void main() { endedStreamController = StreamController(); videoRecordingErrorController = StreamController(); - when(camera.getVideoSize).thenReturn(Size(10, 10)); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); + when(camera.getVideoSize).thenReturn(const Size(10, 10)); + when(camera.initialize) + .thenAnswer((Invocation _) => Future.value()); + when(camera.play).thenAnswer((Invocation _) => Future.value()); when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + when(() => videoElement.onError).thenAnswer((Invocation _) => + FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort).thenAnswer((Invocation _) => + FakeElementStream(abortStreamController.stream)); when(() => camera.onEnded) - .thenAnswer((_) => endedStreamController.stream); + .thenAnswer((Invocation _) => endedStreamController.stream); when(() => camera.onVideoRecordingError) - .thenAnswer((_) => videoRecordingErrorController.stream); + .thenAnswer((Invocation _) => videoRecordingErrorController.stream); - when(() => camera.startVideoRecording()).thenAnswer((_) async => {}); + when(() => camera.startVideoRecording()) + .thenAnswer((Invocation _) async {}); }); testWidgets( 'onCameraInitialized emits a CameraInitializedEvent ' - 'on initializeCamera', (tester) async { + 'on initializeCamera', (WidgetTester tester) async { // Mock the camera to use a blank video stream of size 1280x720. - const videoSize = Size(1280, 720); + const Size videoSize = Size(1280, 720); videoElement = getVideoElementWithBlankStream(videoSize); @@ -2228,9 +2332,9 @@ void main() { any(), cameraId: cameraId, ), - ).thenAnswer((_) async => videoElement.captureStream()); + ).thenAnswer((Invocation _) async => videoElement.captureStream()); - final camera = Camera( + final Camera camera = Camera( textureId: cameraId, cameraService: cameraService, ); @@ -2241,7 +2345,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraInitialized(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); await CameraPlatform.instance.initializeCamera(cameraId); @@ -2264,7 +2369,7 @@ void main() { }); testWidgets('onCameraResolutionChanged emits an empty stream', - (tester) async { + (WidgetTester tester) async { expect( CameraPlatform.instance.onCameraResolutionChanged(cameraId), emits(isEmpty), @@ -2273,14 +2378,15 @@ void main() { testWidgets( 'onCameraClosing emits a CameraClosingEvent ' - 'on the camera ended event', (tester) async { + 'on the camera ended event', (WidgetTester tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; final Stream eventStream = CameraPlatform.instance.onCameraClosing(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); await CameraPlatform.instance.initializeCamera(cameraId); @@ -2289,7 +2395,7 @@ void main() { expect( await streamQueue.next, equals( - CameraClosingEvent(cameraId), + const CameraClosingEvent(cameraId), ), ); @@ -2305,20 +2411,22 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' 'on the camera video error event ' - 'with a message', (tester) async { + 'with a message', (WidgetTester tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); await CameraPlatform.instance.initializeCamera(cameraId); - final error = FakeMediaError( + final FakeMediaError error = FakeMediaError( MediaError.MEDIA_ERR_NETWORK, 'A network error occured.', ); - final errorCode = CameraErrorCode.fromMediaError(error); + final CameraErrorCode errorCode = + CameraErrorCode.fromMediaError(error); when(() => videoElement.error).thenReturn(error); @@ -2329,7 +2437,7 @@ void main() { equals( CameraErrorEvent( cameraId, - 'Error code: ${errorCode}, error message: ${error.message}', + 'Error code: $errorCode, error message: ${error.message}', ), ), ); @@ -2340,16 +2448,19 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' 'on the camera video error event ' - 'with no message', (tester) async { + 'with no message', (WidgetTester tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); await CameraPlatform.instance.initializeCamera(cameraId); - final error = FakeMediaError(MediaError.MEDIA_ERR_NETWORK); - final errorCode = CameraErrorCode.fromMediaError(error); + final FakeMediaError error = + FakeMediaError(MediaError.MEDIA_ERR_NETWORK); + final CameraErrorCode errorCode = + CameraErrorCode.fromMediaError(error); when(() => videoElement.error).thenReturn(error); @@ -2360,7 +2471,7 @@ void main() { equals( CameraErrorEvent( cameraId, - 'Error code: ${errorCode}, error message: No further diagnostic information can be determined or provided.', + 'Error code: $errorCode, error message: No further diagnostic information can be determined or provided.', ), ), ); @@ -2370,11 +2481,12 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on the camera video abort event', (tester) async { + 'on the camera video abort event', (WidgetTester tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); await CameraPlatform.instance.initializeCamera(cameraId); @@ -2385,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.", ), ), ); @@ -2395,8 +2507,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on takePicture error', (tester) async { - final exception = CameraWebException( + 'on takePicture error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -2407,7 +2519,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => await CameraPlatform.instance.takePicture(cameraId), @@ -2431,8 +2544,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on setFlashMode error', (tester) async { - final exception = CameraWebException( + 'on setFlashMode error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -2443,7 +2556,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => await CameraPlatform.instance.setFlashMode( @@ -2470,8 +2584,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on getMaxZoomLevel error', (tester) async { - final exception = CameraWebException( + 'on getMaxZoomLevel error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.zoomLevelNotSupported, 'description', @@ -2482,7 +2596,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => await CameraPlatform.instance.getMaxZoomLevel( @@ -2508,8 +2623,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on getMinZoomLevel error', (tester) async { - final exception = CameraWebException( + 'on getMinZoomLevel error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.zoomLevelNotSupported, 'description', @@ -2520,7 +2635,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => await CameraPlatform.instance.getMinZoomLevel( @@ -2546,8 +2662,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on setZoomLevel error', (tester) async { - final exception = CameraWebException( + 'on setZoomLevel error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.zoomLevelNotSupported, 'description', @@ -2558,7 +2674,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => await CameraPlatform.instance.setZoomLevel( @@ -2585,8 +2702,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on resumePreview error', (tester) async { - final exception = CameraWebException( + 'on resumePreview error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.unknown, 'description', @@ -2597,7 +2714,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => await CameraPlatform.instance.resumePreview(cameraId), @@ -2621,15 +2739,15 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on startVideoRecording error', (tester) async { - final exception = CameraWebException( + 'on startVideoRecording error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', ); when(() => camera.onVideoRecordingError) - .thenAnswer((_) => const Stream.empty()); + .thenAnswer((Invocation _) => const Stream.empty()); when( () => camera.startVideoRecording( @@ -2640,7 +2758,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => @@ -2665,16 +2784,18 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on the camera video recording error event', (tester) async { + 'on the camera video recording error event', + (WidgetTester tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); await CameraPlatform.instance.initializeCamera(cameraId); await CameraPlatform.instance.startVideoRecording(cameraId); - final errorEvent = FakeErrorEvent('type', 'message'); + final FakeErrorEvent errorEvent = FakeErrorEvent('type', 'message'); videoRecordingErrorController.add(errorEvent); @@ -2693,8 +2814,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on stopVideoRecording error', (tester) async { - final exception = CameraWebException( + 'on stopVideoRecording error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -2705,7 +2826,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => @@ -2730,8 +2852,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on pauseVideoRecording error', (tester) async { - final exception = CameraWebException( + 'on pauseVideoRecording error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -2742,7 +2864,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => @@ -2767,8 +2890,8 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on resumeVideoRecording error', (tester) async { - final exception = CameraWebException( + 'on resumeVideoRecording error', (WidgetTester tester) async { + final CameraWebException exception = CameraWebException( cameraId, CameraErrorCode.notStarted, 'description', @@ -2779,7 +2902,8 @@ void main() { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( () async => @@ -2804,18 +2928,21 @@ void main() { }); testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', - (tester) async { - final camera = MockCamera(); - final capturedVideo = MockXFile(); - final stream = Stream.value( - VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)); - when(() => camera.onVideoRecordedEvent).thenAnswer((_) => stream); + (WidgetTester tester) async { + final MockCamera camera = MockCamera(); + final MockXFile capturedVideo = MockXFile(); + final Stream stream = + Stream.value( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)); + when(() => camera.onVideoRecordedEvent) + .thenAnswer((Invocation _) => stream); // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - final streamQueue = - StreamQueue(CameraPlatform.instance.onVideoRecordedEvent(cameraId)); + final StreamQueue streamQueue = + StreamQueue( + CameraPlatform.instance.onVideoRecordedEvent(cameraId)); expect( await streamQueue.next, @@ -2827,7 +2954,8 @@ void main() { group('onDeviceOrientationChanged', () { group('emits an empty stream', () { - testWidgets('when screen is not supported', (tester) async { + testWidgets('when screen is not supported', + (WidgetTester tester) async { when(() => window.screen).thenReturn(null); expect( @@ -2837,7 +2965,7 @@ void main() { }); testWidgets('when screen orientation is not supported', - (tester) async { + (WidgetTester tester) async { when(() => screen.orientation).thenReturn(null); expect( @@ -2848,7 +2976,7 @@ void main() { }); testWidgets('emits the initial DeviceOrientationChangedEvent', - (tester) async { + (WidgetTester tester) async { when( () => cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitPrimary, @@ -2859,20 +2987,22 @@ void main() { when(() => screenOrientation.type) .thenReturn(OrientationType.portraitPrimary); - final eventStreamController = StreamController(); + final StreamController eventStreamController = + StreamController(); when(() => screenOrientation.onChange) - .thenAnswer((_) => eventStreamController.stream); + .thenAnswer((Invocation _) => eventStreamController.stream); final Stream eventStream = CameraPlatform.instance.onDeviceOrientationChanged(); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); expect( await streamQueue.next, equals( - DeviceOrientationChangedEvent( + const DeviceOrientationChangedEvent( DeviceOrientation.portraitUp, ), ), @@ -2883,7 +3013,8 @@ void main() { testWidgets( 'emits a DeviceOrientationChangedEvent ' - 'when the screen orientation is changed', (tester) async { + 'when the screen orientation is changed', + (WidgetTester tester) async { when( () => cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapePrimary, @@ -2896,15 +3027,17 @@ void main() { ), ).thenReturn(DeviceOrientation.portraitDown); - final eventStreamController = StreamController(); + final StreamController eventStreamController = + StreamController(); when(() => screenOrientation.onChange) - .thenAnswer((_) => eventStreamController.stream); + .thenAnswer((Invocation _) => eventStreamController.stream); final Stream eventStream = CameraPlatform.instance.onDeviceOrientationChanged(); - final streamQueue = StreamQueue(eventStream); + final StreamQueue streamQueue = + StreamQueue(eventStream); // Change the screen orientation to landscapePrimary and // emit an event on the screenOrientation.onChange stream. @@ -2916,7 +3049,7 @@ void main() { expect( await streamQueue.next, equals( - DeviceOrientationChangedEvent( + const DeviceOrientationChangedEvent( DeviceOrientation.landscapeLeft, ), ), @@ -2932,7 +3065,7 @@ void main() { expect( await streamQueue.next, equals( - DeviceOrientationChangedEvent( + const DeviceOrientationChangedEvent( DeviceOrientation.portraitDown, ), ), 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 77e9077356f7..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, @@ -160,12 +160,12 @@ class FakeErrorEvent extends Fake implements ErrorEvent { /// final videoStream = videoElement.captureStream(); /// ``` VideoElement getVideoElementWithBlankStream(Size videoSize) { - final canvasElement = CanvasElement( + final CanvasElement canvasElement = CanvasElement( width: videoSize.width.toInt(), height: videoSize.height.toInt(), )..context2D.fillRect(0, 0, videoSize.width, videoSize.height); - final videoElement = VideoElement() + final VideoElement videoElement = VideoElement() ..srcObject = canvasElement.captureStream(); return videoElement; diff --git a/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart index 09de03100871..8614cd95880f 100644 --- a/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart +++ b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart @@ -12,12 +12,12 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('ZoomLevelCapability', () { - testWidgets('sets all properties', (tester) async { - const minimum = 100.0; - const maximum = 400.0; - final videoTrack = MockMediaStreamTrack(); + testWidgets('sets all properties', (WidgetTester tester) async { + const double minimum = 100.0; + const double maximum = 400.0; + final MockMediaStreamTrack videoTrack = MockMediaStreamTrack(); - final capability = ZoomLevelCapability( + final ZoomLevelCapability capability = ZoomLevelCapability( minimum: minimum, maximum: maximum, videoTrack: videoTrack, @@ -28,8 +28,8 @@ void main() { expect(capability.videoTrack, equals(videoTrack)); }); - testWidgets('supports value equality', (tester) async { - final videoTrack = MockMediaStreamTrack(); + testWidgets('supports value equality', (WidgetTester tester) async { + final MockMediaStreamTrack videoTrack = MockMediaStreamTrack(); expect( ZoomLevelCapability( diff --git a/packages/camera/camera_web/example/lib/main.dart b/packages/camera/camera_web/example/lib/main.dart index 6e8f85e74f40..670891fa5009 100644 --- a/packages/camera/camera_web/example/lib/main.dart +++ b/packages/camera/camera_web/example/lib/main.dart @@ -4,13 +4,16 @@ 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 Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml index 1e075712325e..441c6eb7988f 100644 --- a/packages/camera/camera_web/example/pubspec.yaml +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -3,14 +3,13 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter dev_dependencies: - mocktail: ^0.1.4 camera_web: path: ../ flutter_driver: @@ -19,3 +18,4 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + 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 cf0187057188..210a0df59eec 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -49,7 +49,7 @@ class Camera { // A torch mode constraint name. // See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-torch - static const _torchModeKey = "torch"; + static const String _torchModeKey = 'torch'; /// The texture id used to register the camera view. final int textureId; @@ -82,7 +82,8 @@ class Camera { /// The stream controller for the [onEnded] stream. @visibleForTesting - final onEndedController = StreamController.broadcast(); + final StreamController onEndedController = + StreamController.broadcast(); StreamSubscription? _onEndedSubscription; @@ -98,7 +99,7 @@ class Camera { /// The stream controller for the [onVideoRecordingError] stream. @visibleForTesting - final videoRecordingErrorController = + final StreamController videoRecordingErrorController = StreamController.broadcast(); StreamSubscription? _onVideoRecordingErrorSubscription; @@ -124,7 +125,7 @@ class Camera { html.MediaRecorder.isTypeSupported; /// The list of consecutive video data files recorded with [mediaRecorder]. - List _videoData = []; + final List _videoData = []; /// Completes when the video recording is stopped/finished. Completer? _videoAvailableCompleter; @@ -137,8 +138,11 @@ 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 = - (blobs, type) => html.Blob(blobs, type); + (List blobs, String type) => html.Blob(blobs, type); /// The stream that emits a [VideoRecordedEvent] when a video recording is created. Stream get onVideoRecordedEvent => @@ -177,10 +181,10 @@ class Camera { _applyDefaultVideoStyles(videoElement); - final videoTracks = stream!.getVideoTracks(); + final List videoTracks = stream!.getVideoTracks(); if (videoTracks.isNotEmpty) { - final defaultVideoTrack = videoTracks.first; + final html.MediaStreamTrack defaultVideoTrack = videoTracks.first; _onEndedSubscription = defaultVideoTrack.onEnded.listen((html.Event _) { onEndedController.add(defaultVideoTrack); @@ -209,14 +213,14 @@ class Camera { /// Stops the camera stream and resets the camera source. void stop() { - final videoTracks = stream!.getVideoTracks(); + final List videoTracks = stream!.getVideoTracks(); if (videoTracks.isNotEmpty) { onEndedController.add(videoTracks.first); } - final tracks = stream?.getTracks(); + final List? tracks = stream?.getTracks(); if (tracks != null) { - for (final track in tracks) { + for (final html.MediaStreamTrack track in tracks) { track.stop(); } } @@ -229,17 +233,18 @@ class Camera { /// Enables the camera flash (torch mode) for a period of taking a picture /// if the flash mode is either [FlashMode.auto] or [FlashMode.always]. Future takePicture() async { - final shouldEnableTorchMode = + final bool shouldEnableTorchMode = flashMode == FlashMode.auto || flashMode == FlashMode.always; if (shouldEnableTorchMode) { _setTorchMode(enabled: true); } - final videoWidth = videoElement.videoWidth; - final videoHeight = videoElement.videoHeight; - final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); - final isBackCamera = getLensDirection() == CameraLensDirection.back; + final int videoWidth = videoElement.videoWidth; + final int videoHeight = videoElement.videoHeight; + final html.CanvasElement canvas = + html.CanvasElement(width: videoWidth, height: videoHeight); + final bool isBackCamera = getLensDirection() == CameraLensDirection.back; // Flip the picture horizontally if it is not taken from a back camera. if (!isBackCamera) { @@ -251,7 +256,7 @@ class Camera { canvas.context2D .drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); - final blob = await canvas.toBlob('image/jpeg'); + final html.Blob blob = await canvas.toBlob('image/jpeg'); if (shouldEnableTorchMode) { _setTorchMode(enabled: false); @@ -265,17 +270,19 @@ class Camera { /// Returns [Size.zero] if the camera is missing a video track or /// the video track does not include the width or height setting. Size getVideoSize() { - final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + final List videoTracks = + videoElement.srcObject?.getVideoTracks() ?? []; if (videoTracks.isEmpty) { return Size.zero; } - final defaultVideoTrack = videoTracks.first; - final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + final html.MediaStreamTrack defaultVideoTrack = videoTracks.first; + final Map defaultVideoTrackSettings = + defaultVideoTrack.getSettings(); - final width = defaultVideoTrackSettings['width']; - final height = defaultVideoTrackSettings['height']; + final double? width = defaultVideoTrackSettings['width'] as double?; + final double? height = defaultVideoTrackSettings['height'] as double?; if (width != null && height != null) { return Size(width, height); @@ -296,9 +303,11 @@ class Camera { /// Throws a [CameraWebException] if the torch mode is not supported /// or the camera has not been initialized or started. void setFlashMode(FlashMode mode) { - final mediaDevices = window?.navigator.mediaDevices; - final supportedConstraints = mediaDevices?.getSupportedConstraints(); - final torchModeSupported = supportedConstraints?[_torchModeKey] ?? false; + final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; + final Map? supportedConstraints = + mediaDevices?.getSupportedConstraints(); + final bool torchModeSupported = + supportedConstraints?[_torchModeKey] as bool? ?? false; if (!torchModeSupported) { throw CameraWebException( @@ -320,18 +329,19 @@ class Camera { /// Throws a [CameraWebException] if the torch mode is not supported /// or the camera has not been initialized or started. void _setTorchMode({required bool enabled}) { - final videoTracks = stream?.getVideoTracks() ?? []; + final List videoTracks = + stream?.getVideoTracks() ?? []; if (videoTracks.isNotEmpty) { - final defaultVideoTrack = videoTracks.first; + final html.MediaStreamTrack defaultVideoTrack = videoTracks.first; final bool canEnableTorchMode = - defaultVideoTrack.getCapabilities()[_torchModeKey] ?? false; + defaultVideoTrack.getCapabilities()[_torchModeKey] as bool? ?? false; if (canEnableTorchMode) { - defaultVideoTrack.applyConstraints({ - "advanced": [ - { + defaultVideoTrack.applyConstraints({ + 'advanced': [ + { _torchModeKey: enabled, } ] @@ -371,7 +381,7 @@ class Camera { /// Throws a [CameraWebException] if the zoom level is invalid, /// not supported or the camera has not been initialized or started. void setZoomLevel(double zoom) { - final zoomLevelCapability = + final ZoomLevelCapability zoomLevelCapability = _cameraService.getZoomLevelCapabilityForCamera(this); if (zoom < zoomLevelCapability.minimum || @@ -383,9 +393,9 @@ class Camera { ); } - zoomLevelCapability.videoTrack.applyConstraints({ - "advanced": [ - { + zoomLevelCapability.videoTrack.applyConstraints({ + 'advanced': [ + { ZoomLevelCapability.constraintName: zoom, } ] @@ -397,16 +407,19 @@ class Camera { /// Returns null if the camera is missing a video track or /// the video track does not include the facing mode setting. CameraLensDirection? getLensDirection() { - final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + final List videoTracks = + videoElement.srcObject?.getVideoTracks() ?? []; if (videoTracks.isEmpty) { return null; } - final defaultVideoTrack = videoTracks.first; - final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + final html.MediaStreamTrack defaultVideoTrack = videoTracks.first; + final Map defaultVideoTrackSettings = + defaultVideoTrack.getSettings(); - final facingMode = defaultVideoTrackSettings['facingMode']; + final String? facingMode = + defaultVideoTrackSettings['facingMode'] as String?; if (facingMode != null) { return _cameraService.mapFacingModeToLensDirection(facingMode); @@ -432,17 +445,18 @@ class Camera { ); } - mediaRecorder ??= html.MediaRecorder(videoElement.srcObject!, { + mediaRecorder ??= + html.MediaRecorder(videoElement.srcObject!, { 'mimeType': _videoMimeType, }); _videoAvailableCompleter = Completer(); _videoDataAvailableListener = - (event) => _onVideoDataAvailable(event, maxVideoDuration); + (html.Event event) => _onVideoDataAvailable(event, maxVideoDuration); _videoRecordingStoppedListener = - (event) => _onVideoRecordingStopped(event, maxVideoDuration); + (html.Event event) => _onVideoRecordingStopped(event, maxVideoDuration); mediaRecorder!.addEventListener( 'dataavailable', @@ -456,7 +470,7 @@ class Camera { _onVideoRecordingErrorSubscription = mediaRecorder!.onError.listen((html.Event event) { - final error = event as html.ErrorEvent; + final html.ErrorEvent error = event as html.ErrorEvent; if (error != null) { videoRecordingErrorController.add(error); } @@ -474,7 +488,7 @@ class Camera { html.Event event, [ Duration? maxVideoDuration, ]) { - final blob = (event as html.BlobEvent).data; + final html.Blob? blob = (event as html.BlobEvent).data; // Append the recorded part of the video to the list of all video data files. if (blob != null) { @@ -494,11 +508,11 @@ class Camera { ]) async { if (_videoData.isNotEmpty) { // Concatenate all video data files into a single blob. - final videoType = _videoData.first.type; - final videoBlob = blobBuilder(_videoData, videoType); + final String videoType = _videoData.first.type; + final html.Blob videoBlob = blobBuilder(_videoData, videoType); // Create a file containing the video blob. - final file = XFile( + final XFile file = XFile( html.Url.createObjectUrl(videoBlob), mimeType: _videoMimeType, name: videoBlob.hashCode.toString(), @@ -506,7 +520,7 @@ class Camera { // Emit an event containing the recorded video file. videoRecorderController.add( - VideoRecordedEvent(this.textureId, file, maxVideoDuration), + VideoRecordedEvent(textureId, file, maxVideoDuration), ); _videoAvailableCompleter?.complete(file); @@ -594,13 +608,13 @@ class Camera { /// Throws a [CameraWebException] if the browser does not support /// any of the available video mime types. String get _videoMimeType { - const types = [ + const List types = [ 'video/mp4', 'video/webm', ]; return types.firstWhere( - (type) => isVideoTypeSupported(type), + (String type) => isVideoTypeSupported(type), orElse: () => throw CameraWebException( textureId, CameraErrorCode.notSupported, @@ -618,7 +632,7 @@ class Camera { /// Applies default styles to the video [element]. void _applyDefaultVideoStyles(html.VideoElement element) { - final isBackCamera = getLensDirection() == CameraLensDirection.back; + final bool isBackCamera = getLensDirection() == CameraLensDirection.back; // Flip the video horizontally if it is not taken from a back camera. if (!isBackCamera) { diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index 5ba5c80395cc..5f4a5fdde9a4 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -3,6 +3,8 @@ // 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'; @@ -16,7 +18,7 @@ import 'package:flutter/services.dart'; /// obtain the camera stream. class CameraService { // A facing mode constraint name. - static const _facingModeKey = "facingMode"; + static const String _facingModeKey = 'facingMode'; /// The current browser window used to access media devices. @visibleForTesting @@ -32,7 +34,7 @@ class CameraService { CameraOptions options, { int cameraId = 0, }) async { - final mediaDevices = window?.navigator.mediaDevices; + final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; // Throw a not supported exception if the current browser window // does not support any media devices. @@ -44,7 +46,7 @@ class CameraService { } try { - final constraints = await options.toJson(); + final Map constraints = options.toJson(); return await mediaDevices.getUserMedia(constraints); } on html.DomException catch (e) { switch (e.name) { @@ -82,7 +84,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': @@ -120,10 +122,12 @@ class CameraService { ZoomLevelCapability getZoomLevelCapabilityForCamera( Camera camera, ) { - final mediaDevices = window?.navigator.mediaDevices; - final supportedConstraints = mediaDevices?.getSupportedConstraints(); - final zoomLevelSupported = - supportedConstraints?[ZoomLevelCapability.constraintName] ?? false; + final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; + final Map? supportedConstraints = + mediaDevices?.getSupportedConstraints(); + final bool zoomLevelSupported = + supportedConstraints?[ZoomLevelCapability.constraintName] as bool? ?? + false; if (!zoomLevelSupported) { throw CameraWebException( @@ -133,22 +137,26 @@ class CameraService { ); } - final videoTracks = camera.stream?.getVideoTracks() ?? []; + final List videoTracks = + camera.stream?.getVideoTracks() ?? []; if (videoTracks.isNotEmpty) { - final defaultVideoTrack = videoTracks.first; + final html.MediaStreamTrack defaultVideoTrack = videoTracks.first; /// The zoom level capability is represented by MediaSettingsRange. /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaSettingsRange - final zoomLevelCapability = defaultVideoTrack - .getCapabilities()[ZoomLevelCapability.constraintName] ?? - {}; + final Object zoomLevelCapability = defaultVideoTrack + .getCapabilities()[ZoomLevelCapability.constraintName] + as Object? ?? + {}; // The zoom level capability is a nested JS object, therefore // we need to access its properties with the js_util library. // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html - final minimumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'min'); - final maximumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'max'); + final num? minimumZoomLevel = + jsUtil.getProperty(zoomLevelCapability, 'min') as num?; + final num? maximumZoomLevel = + jsUtil.getProperty(zoomLevelCapability, 'max') as num?; if (minimumZoomLevel != null && maximumZoomLevel != null) { return ZoomLevelCapability( @@ -175,7 +183,7 @@ class CameraService { /// Returns a facing mode of the [videoTrack] /// (null if the facing mode is not available). String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { - final mediaDevices = window?.navigator.mediaDevices; + final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; // Throw a not supported exception if the current browser window // does not support any media devices. @@ -187,8 +195,10 @@ class CameraService { } // Check if the camera facing mode is supported by the current browser. - final supportedConstraints = mediaDevices.getSupportedConstraints(); - final facingModeSupported = supportedConstraints[_facingModeKey] ?? false; + final Map supportedConstraints = + mediaDevices.getSupportedConstraints(); + final bool facingModeSupported = + supportedConstraints[_facingModeKey] as bool? ?? false; // Return null if the facing mode is not supported. if (!facingModeSupported) { @@ -201,8 +211,8 @@ class CameraService { // // MediaTrackSettings: // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings - final videoTrackSettings = videoTrack.getSettings(); - final facingMode = videoTrackSettings[_facingModeKey]; + final Map videoTrackSettings = videoTrack.getSettings(); + final String? facingMode = videoTrackSettings[_facingModeKey] as String?; if (facingMode == null) { // If the facing mode does not exist in the video track settings, @@ -220,15 +230,18 @@ class CameraService { return null; } - final videoTrackCapabilities = videoTrack.getCapabilities(); + final Map videoTrackCapabilities = + videoTrack.getCapabilities(); // A list of facing mode capabilities as // the camera may support multiple facing modes. - final facingModeCapabilities = - List.from(videoTrackCapabilities[_facingModeKey] ?? []); + final List facingModeCapabilities = List.from( + (videoTrackCapabilities[_facingModeKey] as List?) + ?.cast() ?? + []); if (facingModeCapabilities.isNotEmpty) { - final facingModeCapability = facingModeCapabilities.first; + final String facingModeCapability = facingModeCapabilities.first; return facingModeCapability; } else { // Return null if there are no facing mode capabilities. @@ -277,16 +290,16 @@ class CameraService { switch (resolutionPreset) { case ResolutionPreset.max: case ResolutionPreset.ultraHigh: - return Size(4096, 2160); + return const Size(4096, 2160); case ResolutionPreset.veryHigh: - return Size(1920, 1080); + return const Size(1920, 1080); case ResolutionPreset.high: - return Size(1280, 720); + return const Size(1280, 720); case ResolutionPreset.medium: - return Size(720, 480); + return const Size(720, 480); case ResolutionPreset.low: default: - return Size(320, 240); + return const Size(320, 240); } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 0021ee47cbde..26f965d49e16 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -40,37 +40,41 @@ class CameraPlugin extends CameraPlatform { /// The cameras managed by the [CameraPlugin]. @visibleForTesting - final cameras = {}; - var _textureCounter = 1; + final Map cameras = {}; + int _textureCounter = 1; /// Metadata associated with each camera description. /// Populated in [availableCameras]. @visibleForTesting - final camerasMetadata = {}; + final Map camerasMetadata = + {}; /// The controller used to broadcast different camera events. /// /// It is `broadcast` as multiple controllers may subscribe /// to different stream views of this controller. @visibleForTesting - final cameraEventStreamController = StreamController.broadcast(); + final StreamController cameraEventStreamController = + StreamController.broadcast(); - final _cameraVideoErrorSubscriptions = - >{}; + final Map> + _cameraVideoErrorSubscriptions = >{}; - final _cameraVideoAbortSubscriptions = - >{}; + final Map> + _cameraVideoAbortSubscriptions = >{}; - final _cameraEndedSubscriptions = + final Map> + _cameraEndedSubscriptions = >{}; - final _cameraVideoRecordingErrorSubscriptions = + final Map> + _cameraVideoRecordingErrorSubscriptions = >{}; /// Returns a stream of camera events for the given [cameraId]. Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream - .where((event) => event.cameraId == cameraId); + .where((CameraEvent event) => event.cameraId == cameraId); /// The current browser window used to access media devices. @visibleForTesting @@ -79,8 +83,8 @@ class CameraPlugin extends CameraPlatform { @override Future> availableCameras() async { try { - final mediaDevices = window?.navigator.mediaDevices; - final cameras = []; + final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; + final List cameras = []; // Throw a not supported exception if the current browser window // does not support any media devices. @@ -92,50 +96,56 @@ class CameraPlugin extends CameraPlatform { } // Request video and audio permissions. - final cameraStream = await _cameraService.getMediaStreamForOptions( - CameraOptions( + final html.MediaStream cameraStream = + await _cameraService.getMediaStreamForOptions( + const CameraOptions( audio: AudioConstraints(enabled: true), ), ); // Release the camera stream used to request video and audio permissions. - cameraStream.getVideoTracks().forEach((videoTrack) => videoTrack.stop()); + cameraStream + .getVideoTracks() + .forEach((html.MediaStreamTrack videoTrack) => videoTrack.stop()); // Request available media devices. - final devices = await mediaDevices.enumerateDevices(); + final List devices = await mediaDevices.enumerateDevices(); // Filter video input devices. - final videoInputDevices = devices + final Iterable videoInputDevices = devices .whereType() - .where((device) => device.kind == MediaDeviceKind.videoInput) + .where((html.MediaDeviceInfo device) => + device.kind == MediaDeviceKind.videoInput) /// The device id property is currently not supported on Internet Explorer: /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility .where( - (device) => device.deviceId != null && device.deviceId!.isNotEmpty, + (html.MediaDeviceInfo device) => + device.deviceId != null && device.deviceId!.isNotEmpty, ); // Map video input devices to camera descriptions. - for (final videoInputDevice in videoInputDevices) { + for (final html.MediaDeviceInfo videoInputDevice in videoInputDevices) { // Get the video stream for the current video input device // to later use for the available video tracks. - final videoStream = await _getVideoStreamForDevice( + final html.MediaStream videoStream = await _getVideoStreamForDevice( videoInputDevice.deviceId!, ); // Get all video tracks in the video stream // to later extract the lens direction from the first track. - final videoTracks = videoStream.getVideoTracks(); + final List videoTracks = + videoStream.getVideoTracks(); if (videoTracks.isNotEmpty) { // Get the facing mode from the first available video track. - final facingMode = + final String? facingMode = _cameraService.getFacingModeForVideoTrack(videoTracks.first); // Get the lens direction based on the facing mode. // Fallback to the external lens direction // if the facing mode is not available. - final lensDirection = facingMode != null + final CameraLensDirection lensDirection = facingMode != null ? _cameraService.mapFacingModeToLensDirection(facingMode) : CameraLensDirection.external; @@ -148,14 +158,14 @@ class CameraPlugin extends CameraPlatform { // https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/label // // Sensor orientation is currently not supported. - final cameraLabel = videoInputDevice.label ?? ''; - final camera = CameraDescription( + final String cameraLabel = videoInputDevice.label ?? ''; + final CameraDescription camera = CameraDescription( name: cameraLabel, lensDirection: lensDirection, sensorOrientation: 0, ); - final cameraMetadata = CameraMetadata( + final CameraMetadata cameraMetadata = CameraMetadata( deviceId: videoInputDevice.deviceId!, facingMode: facingMode, ); @@ -163,6 +173,11 @@ class CameraPlugin extends CameraPlatform { cameras.add(camera); camerasMetadata[camera] = cameraMetadata; + + // Release the camera stream of the current video input device. + for (final html.MediaStreamTrack videoTrack in videoTracks) { + videoTrack.stop(); + } } else { // Ignore as no video tracks exist in the current video input device. continue; @@ -195,22 +210,22 @@ class CameraPlugin extends CameraPlatform { ); } - final textureId = _textureCounter++; + final int textureId = _textureCounter++; - final cameraMetadata = camerasMetadata[cameraDescription]!; + final CameraMetadata cameraMetadata = camerasMetadata[cameraDescription]!; - final cameraType = cameraMetadata.facingMode != null + final CameraType? cameraType = cameraMetadata.facingMode != null ? _cameraService.mapFacingModeToCameraType(cameraMetadata.facingMode!) : null; // Use the highest resolution possible // if the resolution preset is not specified. - final videoSize = _cameraService + final Size videoSize = _cameraService .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); // Create a camera with the given audio and video constraints. // Sensor orientation is currently not supported. - final camera = Camera( + final Camera camera = Camera( textureId: textureId, cameraService: _cameraService, options: CameraOptions( @@ -244,7 +259,7 @@ class CameraPlugin extends CameraPlatform { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) async { try { - final camera = getCamera(cameraId); + final Camera camera = getCamera(cameraId); await camera.initialize(); @@ -255,15 +270,15 @@ class CameraPlugin extends CameraPlatform { // The Event itself (_) doesn't contain information 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 error = camera.videoElement.error!; - final errorCode = CameraErrorCode.fromMediaError(error); - final errorMessage = + final html.MediaError error = camera.videoElement.error!; + final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); + final String? errorMessage = error.message != '' ? error.message : _kDefaultErrorMessage; cameraEventStreamController.add( CameraErrorEvent( cameraId, - 'Error code: ${errorCode}, error message: ${errorMessage}', + 'Error code: $errorCode, error message: $errorMessage', ), ); }); @@ -275,7 +290,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.", ), ); }); @@ -291,17 +306,17 @@ class CameraPlugin extends CameraPlatform { ); }); - final cameraSize = camera.getVideoSize(); + final Size cameraSize = camera.getVideoSize(); cameraEventStreamController.add( CameraInitializedEvent( cameraId, cameraSize.width, cameraSize.height, - // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). + // TODO(bselwe): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). ExposureMode.auto, false, - // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). + // TODO(bselwe): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). FocusMode.auto, false, ), @@ -326,7 +341,7 @@ class CameraPlugin extends CameraPlatform { /// [CameraOptions.video] constraints has to be created and initialized. @override Stream onCameraResolutionChanged(int cameraId) { - return const Stream.empty(); + return const Stream.empty(); } @override @@ -346,42 +361,46 @@ class CameraPlugin extends CameraPlatform { @override Stream onDeviceOrientationChanged() { - final orientation = window?.screen?.orientation; + final html.ScreenOrientation? orientation = window?.screen?.orientation; if (orientation != null) { // Create an initial orientation event that emits the device orientation // as soon as subscribed to this stream. - final initialOrientationEvent = html.Event("change"); + final html.Event initialOrientationEvent = html.Event('change'); return orientation.onChange.startWith(initialOrientationEvent).map( (html.Event _) { - final deviceOrientation = _cameraService + final DeviceOrientation deviceOrientation = _cameraService .mapOrientationTypeToDeviceOrientation(orientation.type!); return DeviceOrientationChangedEvent(deviceOrientation); }, ); } else { - return const Stream.empty(); + return const Stream.empty(); } } @override Future lockCaptureOrientation( int cameraId, - DeviceOrientation deviceOrientation, + DeviceOrientation orientation, ) async { try { - final orientation = window?.screen?.orientation; - final documentElement = window?.document.documentElement; + final html.ScreenOrientation? screenOrientation = + window?.screen?.orientation; + final html.Element? documentElement = window?.document.documentElement; - if (orientation != null && documentElement != null) { - final orientationType = _cameraService - .mapDeviceOrientationToOrientationType(deviceOrientation); + if (screenOrientation != null && documentElement != null) { + final String orientationType = + _cameraService.mapDeviceOrientationToOrientationType(orientation); // Full-screen mode may be required to modify the device orientation. // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api - documentElement.requestFullscreen(); - await orientation.lock(orientationType.toString()); + // Recent versions of Dart changed requestFullscreen to return a Future instead of void. + // This wrapper allows use of both the old and new APIs. + dynamic fullScreen() => documentElement.requestFullscreen(); + await fullScreen(); + await screenOrientation.lock(orientationType); } else { throw PlatformException( code: CameraErrorCode.orientationNotSupported.toString(), @@ -396,8 +415,8 @@ class CameraPlugin extends CameraPlatform { @override Future unlockCaptureOrientation(int cameraId) async { try { - final orientation = window?.screen?.orientation; - final documentElement = window?.document.documentElement; + final html.ScreenOrientation? orientation = window?.screen?.orientation; + final html.Element? documentElement = window?.document.documentElement; if (orientation != null && documentElement != null) { orientation.unlock(); @@ -432,7 +451,7 @@ class CameraPlugin extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { try { - final camera = getCamera(cameraId); + final Camera camera = getCamera(cameraId); // Add camera's video recording errors to the camera events stream. // The error event fires when the video recording is not allowed or an unsupported @@ -459,7 +478,8 @@ class CameraPlugin extends CameraPlatform { @override Future stopVideoRecording(int cameraId) async { try { - final videoRecording = await getCamera(cameraId).stopVideoRecording(); + final XFile videoRecording = + await getCamera(cameraId).stopVideoRecording(); await _cameraVideoRecordingErrorSubscriptions[cameraId]?.cancel(); return videoRecording; } on html.DomException catch (e) { @@ -635,7 +655,7 @@ class CameraPlugin extends CameraPlatform { String deviceId, ) { // Create camera options with the desired device id. - final cameraOptions = CameraOptions( + final CameraOptions cameraOptions = CameraOptions( video: VideoConstraints(deviceId: deviceId), ); @@ -647,7 +667,7 @@ class CameraPlugin extends CameraPlatform { /// Throws a [CameraException] if the camera does not exist. @visibleForTesting Camera getCamera(int cameraId) { - final camera = cameras[cameraId]; + final Camera? camera = cameras[cameraId]; if (camera == null) { throw PlatformException( diff --git a/packages/camera/camera_web/lib/src/shims/dart_js_util.dart b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart index 6601bec6f529..7d766e8c269e 100644 --- a/packages/camera/camera_web/lib/src/shims/dart_js_util.dart +++ b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart @@ -10,5 +10,6 @@ class JsUtil { bool hasProperty(Object o, Object name) => js_util.hasProperty(o, name); /// Returns the value of the property [name] in the object [o]. - dynamic getProperty(Object o, Object name) => js_util.getProperty(o, name); + dynamic getProperty(Object o, Object name) => + js_util.getProperty(o, name); } diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui.dart b/packages/camera/camera_web/lib/src/shims/dart_ui.dart index 5eacec5fe867..3a32721cb9c8 100644 --- a/packages/camera/camera_web/lib/src/shims/dart_ui.dart +++ b/packages/camera/camera_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 -// are exposed from a dedicated place. +// TODO(ditman): Remove this file once web-only dart:ui APIs are exposed from +// a dedicated place. https://github.com/flutter/flutter/issues/55000 export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; 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 f2862af8b704..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 @@ -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/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index f70925b4bede..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. @@ -81,15 +81,15 @@ class CameraErrorCode { static CameraErrorCode fromMediaError(html.MediaError error) { switch (error.code) { case html.MediaError.MEDIA_ERR_ABORTED: - return CameraErrorCode._('mediaErrorAborted'); + return const CameraErrorCode._('mediaErrorAborted'); case html.MediaError.MEDIA_ERR_NETWORK: - return CameraErrorCode._('mediaErrorNetwork'); + return const CameraErrorCode._('mediaErrorNetwork'); case html.MediaError.MEDIA_ERR_DECODE: - return CameraErrorCode._('mediaErrorDecode'); + return const CameraErrorCode._('mediaErrorDecode'); case html.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: - return CameraErrorCode._('mediaErrorSourceNotSupported'); + return const CameraErrorCode._('mediaErrorSourceNotSupported'); default: - return CameraErrorCode._('mediaErrorUnknown'); + return const CameraErrorCode._('mediaErrorUnknown'); } } } 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 c9998e58a52c..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,10 +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'; /// Metadata used along the camera description /// to store additional web-specific camera details. +@immutable class CameraMetadata { /// Creates a new instance of [CameraMetadata] /// with the given [deviceId] and [facingMode]. @@ -25,7 +26,9 @@ class CameraMetadata { @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is CameraMetadata && other.deviceId == deviceId && @@ -33,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 2a4cdbf15348..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,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'; /// Options used to create a camera with the given /// [audio] and [video] media constraints. @@ -12,6 +12,7 @@ import 'dart:ui' show hashValues; /// with audio and video tracks containing the requested types of media. /// /// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints +@immutable class CameraOptions { /// Creates a new instance of [CameraOptions] /// with the given [audio] and [video] constraints. @@ -29,7 +30,7 @@ class CameraOptions { /// Converts the current instance to a Map. Map toJson() { - return { + return { 'audio': audio.toJson(), 'video': video.toJson(), }; @@ -37,7 +38,9 @@ class CameraOptions { @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is CameraOptions && other.audio == audio && @@ -45,12 +48,13 @@ class CameraOptions { } @override - int get hashCode => hashValues(audio, video); + int get hashCode => Object.hash(audio, video); } /// Indicates whether the audio track is requested. /// /// By default, the audio track is not requested. +@immutable class AudioConstraints { /// Creates a new instance of [AudioConstraints] /// with the given [enabled] constraint. @@ -64,7 +68,9 @@ class AudioConstraints { @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is AudioConstraints && other.enabled == enabled; } @@ -75,6 +81,7 @@ class AudioConstraints { /// Defines constraints that the video track must have /// to be considered acceptable. +@immutable class VideoConstraints { /// Creates a new instance of [VideoConstraints] /// with the given constraints. @@ -99,19 +106,29 @@ class VideoConstraints { /// Converts the current instance to a Map. Object toJson() { - final json = {}; - - if (width != null) json['width'] = width!.toJson(); - if (height != null) json['height'] = height!.toJson(); - if (facingMode != null) json['facingMode'] = facingMode!.toJson(); - if (deviceId != null) json['deviceId'] = {'exact': deviceId!}; + final Map json = {}; + + if (width != null) { + json['width'] = width!.toJson(); + } + if (height != null) { + json['height'] = height!.toJson(); + } + if (facingMode != null) { + json['facingMode'] = facingMode!.toJson(); + } + if (deviceId != null) { + json['deviceId'] = {'exact': deviceId!}; + } return json; } @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is VideoConstraints && other.facingMode == facingMode && @@ -121,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]. @@ -146,16 +163,17 @@ class CameraType { } /// Indicates the direction in which the desired camera should be pointing. +@immutable class FacingModeConstraint { - /// Creates a new instance of [FacingModeConstraint] - /// with the given [ideal] and [exact] constraints. - const FacingModeConstraint._({this.ideal, this.exact}); - /// Creates a new instance of [FacingModeConstraint] /// with [ideal] constraint set to [type]. factory FacingModeConstraint(CameraType type) => FacingModeConstraint._(ideal: type); + /// Creates a new instance of [FacingModeConstraint] + /// with the given [ideal] and [exact] constraints. + const FacingModeConstraint._({this.ideal, this.exact}); + /// Creates a new instance of [FacingModeConstraint] /// with [exact] constraint set to [type]. factory FacingModeConstraint.exact(CameraType type) => @@ -174,8 +192,8 @@ class FacingModeConstraint { final CameraType? exact; /// Converts the current instance to a Map. - Object? toJson() { - return { + Object toJson() { + return { if (ideal != null) 'ideal': ideal.toString(), if (exact != null) 'exact': exact.toString(), }; @@ -183,7 +201,9 @@ class FacingModeConstraint { @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is FacingModeConstraint && other.ideal == ideal && @@ -191,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 @@ -200,6 +220,7 @@ class FacingModeConstraint { /// The obtained video track will have a size between [minimum] and [maximum] /// with ideally a size of [ideal]. The size is determined by /// the capabilities of the hardware and the other specified constraints. +@immutable class VideoSizeConstraint { /// Creates a new instance of [VideoSizeConstraint] with the given /// [minimum], [ideal] and [maximum] constraints. @@ -221,18 +242,26 @@ class VideoSizeConstraint { /// Converts the current instance to a Map. Object toJson() { - final json = {}; - - if (ideal != null) json['ideal'] = ideal; - if (minimum != null) json['min'] = minimum; - if (maximum != null) json['max'] = maximum; + final Map json = {}; + + if (ideal != null) { + json['ideal'] = ideal; + } + if (minimum != null) { + json['min'] = minimum; + } + if (maximum != null) { + json['max'] = maximum; + } return json; } @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is VideoSizeConstraint && other.minimum == minimum && @@ -241,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/media_device_kind.dart b/packages/camera/camera_web/lib/src/types/media_device_kind.dart index 1f746808df9e..3607bb260f1e 100644 --- a/packages/camera/camera_web/lib/src/types/media_device_kind.dart +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.dart @@ -7,11 +7,11 @@ /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/kind abstract class MediaDeviceKind { /// A video input media device kind. - static const videoInput = 'videoinput'; + static const String videoInput = 'videoinput'; /// An audio input media device kind. - static const audioInput = 'audioinput'; + static const String audioInput = 'audioinput'; /// An audio output media device kind. - static const audioOutput = 'audiooutput'; + static const String audioOutput = 'audiooutput'; } 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 ace57140d956..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,15 +3,17 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:ui' show hashValues; + +import 'package:flutter/foundation.dart'; /// The possible range of values for the zoom level configurable /// on the camera video track. +@immutable class ZoomLevelCapability { /// Creates a new instance of [ZoomLevelCapability] with the given /// zoom level range of [minimum] to [maximum] configurable /// on the [videoTrack]. - ZoomLevelCapability({ + const ZoomLevelCapability({ required this.minimum, required this.maximum, required this.videoTrack, @@ -19,7 +21,7 @@ class ZoomLevelCapability { /// The zoom level constraint name. /// See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-zoom - static const constraintName = "zoom"; + static const String constraintName = 'zoom'; /// The minimum zoom level. final double minimum; @@ -32,7 +34,9 @@ class ZoomLevelCapability { @override bool operator ==(Object other) { - if (identical(this, other)) return true; + if (identical(this, other)) { + return true; + } return other is ZoomLevelCapability && other.minimum == minimum && @@ -41,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 f001fe92365b..68aa79181ce4 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -1,12 +1,12 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. -repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_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 +version: 0.3.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -27,4 +27,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 diff --git a/packages/camera/camera_windows/.gitignore b/packages/camera/camera_windows/.gitignore new file mode 100644 index 000000000000..e9dc58d3d6e2 --- /dev/null +++ b/packages/camera/camera_windows/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/camera/camera_windows/.metadata b/packages/camera/camera_windows/.metadata new file mode 100644 index 000000000000..5bed5265e818 --- /dev/null +++ b/packages/camera/camera_windows/.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: 18116933e77adc82f80866c928266a5b4f1ed645 + channel: stable + +project_type: plugin diff --git a/packages/camera/camera_windows/AUTHORS b/packages/camera/camera_windows/AUTHORS new file mode 100644 index 000000000000..b2178a5e8444 --- /dev/null +++ b/packages/camera/camera_windows/AUTHORS @@ -0,0 +1,8 @@ +# 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. +Joonas Kerttula +Codemate Ltd. diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md new file mode 100644 index 000000000000..f84e442c68da --- /dev/null +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -0,0 +1,13 @@ +## 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/battery/battery/LICENSE b/packages/camera/camera_windows/LICENSE similarity index 100% rename from packages/battery/battery/LICENSE rename to packages/camera/camera_windows/LICENSE diff --git a/packages/camera/camera_windows/README.md b/packages/camera/camera_windows/README.md new file mode 100644 index 000000000000..dc27bcc85e9d --- /dev/null +++ b/packages/camera/camera_windows/README.md @@ -0,0 +1,66 @@ +# Camera Windows Plugin + +The Windows implementation of [`camera`][camera]. + +*Note*: This plugin is under development. +See [missing implementations and limitations](#missing-features-on-the-windows-platform). + +## Usage + +### 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]. + +## Missing features on the Windows platform + +### Device orientation + +Device orientation detection +is not yet implemented: [issue #97540][device-orientation-issue]. + +### Pause and Resume video recording + +Pausing and resuming the video recording +is not supported due to Windows API limitations. + +### Exposure mode, point and offset + +Support for explosure mode and offset +is not yet implemented: [issue #97537][camera-control-issue]. + +Exposure points are not supported due to +limitations of the Windows API. + +### Focus mode and point + +Support for explosure mode and offset +is not yet implemented: [issue #97537][camera-control-issue]. + +### Flash mode + +Support for flash mode is not yet implemented: [issue #97537][camera-control-issue]. + +Focus points are not supported due to +current limitations of the Windows API. + +### Streaming of frames + +Support for image streaming is not yet implemented: [issue #97542][image-streams-issue]. + +## Error handling + +Camera errors can be listened using the platform's `onCameraError` method. + +Listening to errors is important, and in certain situations, +disposing of the camera is the only way to reset the situation. + + + +[camera]: https://pub.dev/packages/camera +[endorsed-federated-plugin]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[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 diff --git a/packages/camera/camera_windows/example/.gitignore b/packages/camera/camera_windows/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/.metadata b/packages/camera/camera_windows/example/.metadata new file mode 100644 index 000000000000..a5584fc372d9 --- /dev/null +++ b/packages/camera/camera_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: 18116933e77adc82f80866c928266a5b4f1ed645 + channel: stable + +project_type: app diff --git a/packages/camera/camera_windows/example/README.md b/packages/camera/camera_windows/example/README.md new file mode 100644 index 000000000000..ee7326472eaf --- /dev/null +++ b/packages/camera/camera_windows/example/README.md @@ -0,0 +1,3 @@ +# camera_windows_example + +Demonstrates how to use the camera_windows plugin. diff --git a/packages/camera/camera_windows/example/integration_test/camera_test.dart b/packages/camera/camera_windows/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..cda0f402de6c --- /dev/null +++ b/packages/camera/camera_windows/example/integration_test/camera_test.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. + +import 'dart:async'; +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +// Note that these integration tests do not currently cover +// most features and code paths, as they can only be tested if +// one or more cameras are available in the test environment. +// Native unit tests with better coverage are available at +// the native part of the plugin implementation. + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('initializeCamera', () { + testWidgets('throws exception if camera is not created', + (WidgetTester _) async { + final CameraPlatform camera = CameraPlatform.instance; + + expect(() async => await camera.initializeCamera(1234), + throwsA(isA())); + }); + }); + + group('takePicture', () { + testWidgets('throws exception if camera is not created', + (WidgetTester _) async { + final CameraPlatform camera = CameraPlatform.instance; + + expect(() async => await camera.takePicture(1234), + throwsA(isA())); + }); + }); + + group('startVideoRecording', () { + testWidgets('throws exception if camera is not created', + (WidgetTester _) async { + final CameraPlatform camera = CameraPlatform.instance; + + expect(() async => await camera.startVideoRecording(1234), + throwsA(isA())); + }); + }); + + group('stopVideoRecording', () { + testWidgets('throws exception if camera is not created', + (WidgetTester _) async { + final CameraPlatform camera = CameraPlatform.instance; + + expect(() async => await camera.stopVideoRecording(1234), + throwsA(isA())); + }); + }); + + group('pausePreview', () { + testWidgets('throws exception if camera is not created', + (WidgetTester _) async { + final CameraPlatform camera = CameraPlatform.instance; + + expect(() async => await camera.pausePreview(1234), + throwsA(isA())); + }); + }); + + group('resumePreview', () { + testWidgets('throws exception if camera is not created', + (WidgetTester _) async { + final CameraPlatform camera = CameraPlatform.instance; + + expect(() async => await camera.resumePreview(1234), + throwsA(isA())); + }); + }); + + group('onDeviceOrientationChanged', () { + testWidgets('emits the initial DeviceOrientationChangedEvent', + (WidgetTester _) async { + final Stream eventStream = + CameraPlatform.instance.onDeviceOrientationChanged(); + + final StreamQueue streamQueue = + StreamQueue(eventStream); + + expect( + await streamQueue.next, + equals( + const DeviceOrientationChangedEvent( + DeviceOrientation.landscapeRight, + ), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_windows/example/lib/main.dart b/packages/camera/camera_windows/example/lib/main.dart new file mode 100644 index 000000000000..5758b0f1e397 --- /dev/null +++ b/packages/camera/camera_windows/example/lib/main.dart @@ -0,0 +1,455 @@ +// Copyright 2013 The Flutter Authors. 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/material.dart'; +import 'package:flutter/services.dart'; + +void main() { + 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(); +} + +class _MyAppState extends State { + String _cameraInfo = 'Unknown'; + List _cameras = []; + int _cameraIndex = 0; + int _cameraId = -1; + bool _initialized = false; + bool _recording = false; + bool _recordingTimed = false; + bool _recordAudio = true; + bool _previewPaused = false; + Size? _previewSize; + ResolutionPreset _resolutionPreset = ResolutionPreset.veryHigh; + StreamSubscription? _errorStreamSubscription; + StreamSubscription? _cameraClosingStreamSubscription; + + @override + void initState() { + super.initState(); + WidgetsFlutterBinding.ensureInitialized(); + _fetchCameras(); + } + + @override + void dispose() { + _disposeCurrentCamera(); + _errorStreamSubscription?.cancel(); + _errorStreamSubscription = null; + _cameraClosingStreamSubscription?.cancel(); + _cameraClosingStreamSubscription = null; + super.dispose(); + } + + /// Fetches list of available cameras from camera_windows plugin. + Future _fetchCameras() async { + String cameraInfo; + List cameras = []; + + int cameraIndex = 0; + try { + cameras = await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + cameraInfo = 'No available cameras'; + } else { + cameraIndex = _cameraIndex % cameras.length; + cameraInfo = 'Found camera: ${cameras[cameraIndex].name}'; + } + } on PlatformException catch (e) { + cameraInfo = 'Failed to get cameras: ${e.code}: ${e.message}'; + } + + if (mounted) { + setState(() { + _cameraIndex = cameraIndex; + _cameras = cameras; + _cameraInfo = cameraInfo; + }); + } + } + + /// Initializes the camera on the device. + Future _initializeCamera() async { + assert(!_initialized); + + if (_cameras.isEmpty) { + return; + } + + int cameraId = -1; + try { + final int cameraIndex = _cameraIndex % _cameras.length; + final CameraDescription camera = _cameras[cameraIndex]; + + cameraId = await CameraPlatform.instance.createCamera( + camera, + _resolutionPreset, + enableAudio: _recordAudio, + ); + + _errorStreamSubscription?.cancel(); + _errorStreamSubscription = CameraPlatform.instance + .onCameraError(cameraId) + .listen(_onCameraError); + + _cameraClosingStreamSubscription?.cancel(); + _cameraClosingStreamSubscription = CameraPlatform.instance + .onCameraClosing(cameraId) + .listen(_onCameraClosing); + + final Future initialized = + CameraPlatform.instance.onCameraInitialized(cameraId).first; + + await CameraPlatform.instance.initializeCamera( + cameraId, + imageFormatGroup: ImageFormatGroup.unknown, + ); + + final CameraInitializedEvent event = await initialized; + _previewSize = Size( + event.previewWidth, + event.previewHeight, + ); + + if (mounted) { + setState(() { + _initialized = true; + _cameraId = cameraId; + _cameraIndex = cameraIndex; + _cameraInfo = 'Capturing camera: ${camera.name}'; + }); + } + } on CameraException catch (e) { + try { + if (cameraId >= 0) { + await CameraPlatform.instance.dispose(cameraId); + } + } on CameraException catch (e) { + debugPrint('Failed to dispose camera: ${e.code}: ${e.description}'); + } + + // Reset state. + if (mounted) { + setState(() { + _initialized = false; + _cameraId = -1; + _cameraIndex = 0; + _previewSize = null; + _recording = false; + _recordingTimed = false; + _cameraInfo = + 'Failed to initialize camera: ${e.code}: ${e.description}'; + }); + } + } + } + + Future _disposeCurrentCamera() async { + if (_cameraId >= 0 && _initialized) { + try { + await CameraPlatform.instance.dispose(_cameraId); + + if (mounted) { + setState(() { + _initialized = false; + _cameraId = -1; + _previewSize = null; + _recording = false; + _recordingTimed = false; + _previewPaused = false; + _cameraInfo = 'Camera disposed'; + }); + } + } on CameraException catch (e) { + if (mounted) { + setState(() { + _cameraInfo = + 'Failed to dispose camera: ${e.code}: ${e.description}'; + }); + } + } + } + } + + Widget _buildPreview() { + return CameraPlatform.instance.buildPreview(_cameraId); + } + + Future _takePicture() async { + final XFile _file = await CameraPlatform.instance.takePicture(_cameraId); + _showInSnackBar('Picture captured to: ${_file.path}'); + } + + Future _recordTimed(int seconds) async { + if (_initialized && _cameraId > 0 && !_recordingTimed) { + CameraPlatform.instance + .onVideoRecordedEvent(_cameraId) + .first + .then((VideoRecordedEvent event) async { + if (mounted) { + setState(() { + _recordingTimed = false; + }); + + _showInSnackBar('Video captured to: ${event.file.path}'); + } + }); + + await CameraPlatform.instance.startVideoRecording( + _cameraId, + maxVideoDuration: Duration(seconds: seconds), + ); + + if (mounted) { + setState(() { + _recordingTimed = true; + }); + } + } + } + + Future _toggleRecord() async { + if (_initialized && _cameraId > 0) { + if (_recordingTimed) { + /// Request to stop timed recording short. + await CameraPlatform.instance.stopVideoRecording(_cameraId); + } else { + if (!_recording) { + await CameraPlatform.instance.startVideoRecording(_cameraId); + } else { + final XFile _file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + + _showInSnackBar('Video captured to: ${_file.path}'); + } + + if (mounted) { + setState(() { + _recording = !_recording; + }); + } + } + } + } + + Future _togglePreview() async { + if (_initialized && _cameraId >= 0) { + if (!_previewPaused) { + await CameraPlatform.instance.pausePreview(_cameraId); + } else { + await CameraPlatform.instance.resumePreview(_cameraId); + } + if (mounted) { + setState(() { + _previewPaused = !_previewPaused; + }); + } + } + } + + Future _switchCamera() async { + if (_cameras.isNotEmpty) { + // select next index; + _cameraIndex = (_cameraIndex + 1) % _cameras.length; + if (_initialized && _cameraId >= 0) { + await _disposeCurrentCamera(); + await _fetchCameras(); + if (_cameras.isNotEmpty) { + await _initializeCamera(); + } + } else { + await _fetchCameras(); + } + } + } + + Future _onResolutionChange(ResolutionPreset newValue) async { + setState(() { + _resolutionPreset = newValue; + }); + if (_initialized && _cameraId >= 0) { + // Re-inits camera with new resolution preset. + await _disposeCurrentCamera(); + await _initializeCamera(); + } + } + + Future _onAudioChange(bool recordAudio) async { + setState(() { + _recordAudio = recordAudio; + }); + if (_initialized && _cameraId >= 0) { + // Re-inits camera with new record audio setting. + await _disposeCurrentCamera(); + await _initializeCamera(); + } + } + + void _onCameraError(CameraErrorEvent event) { + if (mounted) { + _scaffoldMessengerKey.currentState?.showSnackBar( + SnackBar(content: Text('Error: ${event.description}'))); + + // Dispose camera on camera error as it can not be used anymore. + _disposeCurrentCamera(); + _fetchCameras(); + } + } + + void _onCameraClosing(CameraClosingEvent event) { + if (mounted) { + _showInSnackBar('Camera is closing'); + } + } + + void _showInSnackBar(String message) { + _scaffoldMessengerKey.currentState?.showSnackBar(SnackBar( + content: Text(message), + duration: const Duration(seconds: 1), + )); + } + + final GlobalKey _scaffoldMessengerKey = + GlobalKey(); + + @override + Widget build(BuildContext context) { + final List> resolutionItems = + ResolutionPreset.values + .map>((ResolutionPreset value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList(); + + return MaterialApp( + scaffoldMessengerKey: _scaffoldMessengerKey, + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 5, + horizontal: 10, + ), + child: Text(_cameraInfo), + ), + if (_cameras.isEmpty) + ElevatedButton( + onPressed: _fetchCameras, + child: const Text('Re-check available cameras'), + ), + if (_cameras.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + value: _resolutionPreset, + onChanged: (ResolutionPreset? value) { + if (value != null) { + _onResolutionChange(value); + } + }, + items: resolutionItems, + ), + const SizedBox(width: 20), + const Text('Audio:'), + Switch( + value: _recordAudio, + onChanged: (bool state) => _onAudioChange(state)), + const SizedBox(width: 20), + ElevatedButton( + onPressed: _initialized + ? _disposeCurrentCamera + : _initializeCamera, + child: + Text(_initialized ? 'Dispose camera' : 'Create camera'), + ), + const SizedBox(width: 5), + ElevatedButton( + onPressed: _initialized ? _takePicture : null, + child: const Text('Take picture'), + ), + const SizedBox(width: 5), + ElevatedButton( + onPressed: _initialized ? _togglePreview : null, + child: Text( + _previewPaused ? 'Resume preview' : 'Pause preview', + ), + ), + const SizedBox(width: 5), + ElevatedButton( + onPressed: _initialized ? _toggleRecord : null, + child: Text( + (_recording || _recordingTimed) + ? 'Stop recording' + : 'Record Video', + ), + ), + const SizedBox(width: 5), + ElevatedButton( + onPressed: (_initialized && !_recording && !_recordingTimed) + ? () => _recordTimed(5) + : null, + child: const Text( + 'Record 5 seconds', + ), + ), + if (_cameras.length > 1) ...[ + const SizedBox(width: 5), + ElevatedButton( + onPressed: _switchCamera, + child: const Text( + 'Switch camera', + ), + ), + ] + ], + ), + const SizedBox(height: 5), + if (_initialized && _cameraId > 0 && _previewSize != null) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), + child: Align( + alignment: Alignment.center, + child: Container( + constraints: const BoxConstraints( + maxHeight: 500, + ), + child: AspectRatio( + aspectRatio: _previewSize!.width / _previewSize!.height, + child: _buildPreview(), + ), + ), + ), + ), + if (_previewSize != null) + Center( + child: Text( + 'Preview size: ${_previewSize!.width.toStringAsFixed(0)}x${_previewSize!.height.toStringAsFixed(0)}', + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/camera/camera_windows/example/pubspec.yaml b/packages/camera/camera_windows/example/pubspec.yaml new file mode 100644 index 000000000000..aa806a292333 --- /dev/null +++ b/packages/camera/camera_windows/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: camera_windows_example +description: Demonstrates how to use the camera_windows plugin. +publish_to: 'none' + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + camera_platform_interface: ^2.1.2 + camera_windows: + # When depending on this package from a real application you should use: + # camera_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: ../ + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart b/packages/camera/camera_windows/example/test_driver/integration_test.dart similarity index 100% rename from packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart rename to packages/camera/camera_windows/example/test_driver/integration_test.dart diff --git a/packages/camera/camera_windows/example/windows/.gitignore b/packages/camera/camera_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/CMakeLists.txt b/packages/camera/camera_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..28757c79ca2f --- /dev/null +++ b/packages/camera/camera_windows/example/windows/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.14) +project(camera_windows_example LANGUAGES CXX) + +set(BINARY_NAME "camera_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_camera_windows_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS camera_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/camera/camera_windows/example/windows/flutter/CMakeLists.txt b/packages/camera/camera_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..b2e4bd8d658b --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/flutter/generated_plugins.cmake b/packages/camera/camera_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..458d22dac410 --- /dev/null +++ b/packages/camera/camera_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + camera_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/camera/camera_windows/example/windows/runner/CMakeLists.txt b/packages/camera/camera_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..adb2052b6050 --- /dev/null +++ b/packages/camera/camera_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +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/camera/camera_windows/example/windows/runner/Runner.rc b/packages/camera/camera_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..f1cfa4391ebd --- /dev/null +++ b/packages/camera/camera_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", "Demonstrates how to use the camera_windows plugin." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "camera_windows_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "camera_windows_example.exe" "\0" + VALUE "ProductName", "camera_windows_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/camera/camera_windows/example/windows/runner/flutter_window.cpp b/packages/camera/camera_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8254bd9ff3c1 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/runner/flutter_window.h b/packages/camera/camera_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/runner/main.cpp b/packages/camera/camera_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..755a90b42f19 --- /dev/null +++ b/packages/camera/camera_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"camera_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/camera/camera_windows/example/windows/runner/resource.h b/packages/camera/camera_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/runner/resources/app_icon.ico b/packages/camera/camera_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/camera/camera_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/camera/camera_windows/example/windows/runner/runner.exe.manifest b/packages/camera/camera_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/camera/camera_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/camera/camera_windows/example/windows/runner/utils.cpp b/packages/camera/camera_windows/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..fb7e945a63b7 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/runner/utils.h b/packages/camera/camera_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/runner/win32_window.cpp b/packages/camera/camera_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..85aa3614e8ad --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/example/windows/runner/win32_window.h b/packages/camera/camera_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart new file mode 100644 index 000000000000..d998863d43a7 --- /dev/null +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -0,0 +1,432 @@ +// Copyright 2013 The Flutter Authors. 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/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +/// An implementation of [CameraPlatform] for Windows. +class CameraWindows extends CameraPlatform { + /// Registers the Windows implementation of CameraPlatform. + static void registerWith() { + CameraPlatform.instance = CameraWindows(); + } + + /// The method channel used to interact with the native platform. + @visibleForTesting + final MethodChannel pluginChannel = + const MethodChannel('plugins.flutter.io/camera_windows'); + + /// Camera specific method channels to allow comminicating with specific cameras. + final Map _cameraChannels = {}; + + /// The controller that broadcasts events coming from handleCameraMethodCall + /// + /// 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(); + + /// Returns a stream of camera events for the given [cameraId]. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((CameraEvent event) => event.cameraId == cameraId); + + @override + Future> availableCameras() async { + try { + final List>? cameras = await pluginChannel + .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 { + // If resolutionPreset is not specified, plugin selects the highest resolution possible. + final Map? reply = await pluginChannel + .invokeMapMethod('create', { + 'cameraName': cameraDescription.name, + 'resolutionPreset': _serializeResolutionPreset(resolutionPreset), + 'enableAudio': enableAudio, + }); + + if (reply == null) { + throw CameraException('System', 'Cannot create camera'); + } + + return reply['cameraId']! as int; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + final int requestedCameraId = cameraId; + + /// Creates channel for camera events. + _cameraChannels.putIfAbsent(requestedCameraId, () { + final MethodChannel channel = MethodChannel( + 'plugins.flutter.io/camera_windows/camera$requestedCameraId'); + channel.setMethodCallHandler( + (MethodCall call) => handleCameraMethodCall(call, requestedCameraId), + ); + return channel; + }); + + final Map? reply; + try { + reply = await pluginChannel.invokeMapMethod( + 'initialize', + { + 'cameraId': requestedCameraId, + }, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + cameraEventStreamController.add( + CameraInitializedEvent( + requestedCameraId, + reply!['previewWidth']!, + reply['previewHeight']!, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), + ); + } + + @override + Future dispose(int cameraId) async { + await pluginChannel.invokeMethod( + 'dispose', + {'cameraId': cameraId}, + ); + + // Destroy method channel after camera is disposed to be able to handle last messages. + if (_cameraChannels.containsKey(cameraId)) { + final MethodChannel? cameraChannel = _cameraChannels[cameraId]; + cameraChannel?.setMethodCallHandler(null); + _cameraChannels.remove(cameraId); + } + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + /// Windows API does not automatically change the camera's resolution + /// during capture so these events are never send from the platform. + /// Support for changing resolution should be implemented, if support for + /// requesting resolution change is added to camera platform interface. + return const Stream.empty(); + } + + @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() { + // TODO(jokerttu): Implement device orientation detection, https://github.com/flutter/flutter/issues/97540. + // Force device orientation to landscape as by default camera plugin uses portraitUp orientation. + return Stream.value( + const DeviceOrientationChangedEvent(DeviceOrientation.landscapeRight), + ); + } + + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation orientation, + ) async { + // TODO(jokerttu): Implement lock capture orientation feature, https://github.com/flutter/flutter/issues/97540. + throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + // TODO(jokerttu): Implement unlock capture orientation feature, https://github.com/flutter/flutter/issues/97540. + throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + } + + @override + Future takePicture(int cameraId) async { + final String? path; + path = await pluginChannel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + + return XFile(path!); + } + + @override + Future prepareForVideoRecording() => + pluginChannel.invokeMethod('prepareForVideoRecording'); + + @override + Future startVideoRecording( + int cameraId, { + Duration? maxVideoDuration, + }) async { + await pluginChannel.invokeMethod( + 'startVideoRecording', + { + 'cameraId': cameraId, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + }, + ); + } + + @override + Future stopVideoRecording(int cameraId) async { + final String? path; + + path = await pluginChannel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + + return XFile(path!); + } + + @override + Future pauseVideoRecording(int cameraId) async { + throw UnsupportedError( + 'pauseVideoRecording() is not supported due to Win32 API limitations.'); + } + + @override + Future resumeVideoRecording(int cameraId) async { + throw UnsupportedError( + 'resumeVideoRecording() is not supported due to Win32 API limitations.'); + } + + @override + Future setFlashMode(int cameraId, FlashMode mode) async { + // TODO(jokerttu): Implement flash mode support, https://github.com/flutter/flutter/issues/97537. + throw UnimplementedError('setFlashMode() is not implemented.'); + } + + @override + Future setExposureMode(int cameraId, ExposureMode mode) async { + // TODO(jokerttu): Implement explosure mode support, https://github.com/flutter/flutter/issues/97537. + throw UnimplementedError('setExposureMode() is not implemented.'); + } + + @override + Future setExposurePoint(int cameraId, Point? point) async { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + throw UnsupportedError( + 'setExposurePoint() is not supported due to Win32 API limitations.'); + } + + @override + Future getMinExposureOffset(int cameraId) async { + // TODO(jokerttu): Implement exposure control support, https://github.com/flutter/flutter/issues/97537. + // Value is returned to support existing implementations. + return 0.0; + } + + @override + Future getMaxExposureOffset(int cameraId) async { + // TODO(jokerttu): Implement exposure control support, https://github.com/flutter/flutter/issues/97537. + // Value is returned to support existing implementations. + return 0.0; + } + + @override + Future getExposureOffsetStepSize(int cameraId) async { + // TODO(jokerttu): Implement exposure control support, https://github.com/flutter/flutter/issues/97537. + // Value is returned to support existing implementations. + return 1.0; + } + + @override + Future setExposureOffset(int cameraId, double offset) async { + // TODO(jokerttu): Implement exposure control support, https://github.com/flutter/flutter/issues/97537. + throw UnimplementedError('setExposureOffset() is not implemented.'); + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) async { + // TODO(jokerttu): Implement focus mode support, https://github.com/flutter/flutter/issues/97537. + throw UnimplementedError('setFocusMode() is not implemented.'); + } + + @override + Future setFocusPoint(int cameraId, Point? point) async { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + throw UnsupportedError( + 'setFocusPoint() is not supported due to Win32 API limitations.'); + } + + @override + Future getMinZoomLevel(int cameraId) async { + // TODO(jokerttu): Implement zoom level support, https://github.com/flutter/flutter/issues/97537. + // Value is returned to support existing implementations. + return 1.0; + } + + @override + Future getMaxZoomLevel(int cameraId) async { + // TODO(jokerttu): Implement zoom level support, https://github.com/flutter/flutter/issues/97537. + // Value is returned to support existing implementations. + return 1.0; + } + + @override + Future setZoomLevel(int cameraId, double zoom) async { + // TODO(jokerttu): Implement zoom level support, https://github.com/flutter/flutter/issues/97537. + throw UnimplementedError('setZoomLevel() is not implemented.'); + } + + @override + Future pausePreview(int cameraId) async { + await pluginChannel.invokeMethod( + 'pausePreview', + {'cameraId': cameraId}, + ); + } + + @override + Future resumePreview(int cameraId) async { + await pluginChannel.invokeMethod( + 'resumePreview', + {'cameraId': cameraId}, + ); + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the resolution preset as a nullable String. + String? _serializeResolutionPreset(ResolutionPreset? resolutionPreset) { + switch (resolutionPreset) { + case null: + return null; + 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'; + } + } + + /// 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 'camera_closing': + cameraEventStreamController.add( + CameraClosingEvent( + cameraId, + ), + ); + break; + case 'video_recorded': + // This is called if maxVideoDuration was given on record start. + 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 UnimplementedError(); + } + } + + /// Parses string presentation of the camera lens direction and returns enum value. + @visibleForTesting + 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'); + } +} diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml new file mode 100644 index 000000000000..b519668c431a --- /dev/null +++ b/packages/camera/camera_windows/pubspec.yaml @@ -0,0 +1,29 @@ +name: camera_windows +description: A Flutter plugin for getting information about and controlling the camera on 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.1.0+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: camera + platforms: + windows: + pluginClass: CameraWindows + dartPluginClass: CameraWindows + +dependencies: + camera_platform_interface: ^2.1.2 + cross_file: ^0.3.1 + flutter: + sdk: flutter + stream_transform: ^2.0.0 + +dev_dependencies: + async: ^2.5.0 + flutter_test: + sdk: flutter diff --git a/packages/camera/camera_windows/test/camera_windows_test.dart b/packages/camera/camera_windows/test/camera_windows_test.dart new file mode 100644 index 000000000000..c1a0fe40325f --- /dev/null +++ b/packages/camera/camera_windows/test/camera_windows_test.dart @@ -0,0 +1,664 @@ +// Copyright 2013 The Flutter Authors. 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_windows/camera_windows.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import './utils/method_channel_mock.dart'; + +void main() { + const String pluginChannelName = 'plugins.flutter.io/camera_windows'; + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$CameraWindows()', () { + test('registered instance', () { + CameraWindows.registerWith(); + expect(CameraPlatform.instance, isA()); + }); + + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: pluginChannelName, + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + } + }); + final CameraWindows plugin = CameraWindows(); + + // Act + final int cameraId = await plugin.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + 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: pluginChannelName, + methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final CameraWindows plugin = CameraWindows(); + + // Act + expect( + () => plugin.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: pluginChannelName, + methods: { + 'initialize': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }, + ); + final CameraWindows plugin = CameraWindows(); + + // Act + expect( + () => plugin.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: pluginChannelName, + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + 'initialize': { + 'previewWidth': 1920.toDouble(), + 'previewHeight': 1080.toDouble() + }, + }); + final CameraWindows plugin = CameraWindows(); + final int cameraId = await plugin.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + + // Act + await plugin.initializeCamera(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + isMethodCall( + 'initialize', + arguments: {'cameraId': 1}, + ), + ]); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + final MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: pluginChannelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': { + 'previewWidth': 1920.toDouble(), + 'previewHeight': 1080.toDouble() + }, + 'dispose': {'cameraId': 1} + }); + + final CameraWindows plugin = CameraWindows(); + final int cameraId = await plugin.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + await plugin.initializeCamera(cameraId); + + // Act + await plugin.dispose(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + anything, + isMethodCall( + 'dispose', + arguments: {'cameraId': 1}, + ), + ]); + }); + }); + + group('Event Tests', () { + late CameraWindows plugin; + late int cameraId; + setUp(() async { + MethodChannelMock( + channelName: pluginChannelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': { + 'previewWidth': 1920.toDouble(), + 'previewHeight': 1080.toDouble() + }, + }, + ); + + plugin = CameraWindows(); + cameraId = await plugin.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + await plugin.initializeCamera(cameraId); + }); + + test('Should receive camera closing events', () async { + // Act + final Stream eventStream = + plugin.onCameraClosing(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + // Emit test events + final CameraClosingEvent event = CameraClosingEvent(cameraId); + await plugin.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await plugin.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await plugin.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 = + plugin.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(errorStream); + + // Emit test events + final CameraErrorEvent event = + CameraErrorEvent(cameraId, 'Error Description'); + await plugin.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await plugin.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await plugin.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(); + }); + }); + + group('Function Tests', () { + late CameraWindows plugin; + late int cameraId; + + setUp(() async { + MethodChannelMock( + channelName: pluginChannelName, + methods: { + 'create': {'cameraId': 1}, + 'initialize': { + 'previewWidth': 1920.toDouble(), + 'previewHeight': 1080.toDouble() + }, + }, + ); + plugin = CameraWindows(); + cameraId = await plugin.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + await plugin.initializeCamera(cameraId); + }); + + 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: pluginChannelName, + methods: {'availableCameras': returnData}, + ); + + // Act + final List cameras = await plugin.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: plugin.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: pluginChannelName, + methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + + // Act + expect( + plugin.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: pluginChannelName, + methods: {'takePicture': '/test/path.jpg'}); + + // Act + final XFile file = await plugin.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: pluginChannelName, + methods: {'prepareForVideoRecording': null}, + ); + + // Act + await plugin.prepareForVideoRecording(); + + // Assert + expect(channel.log, [ + isMethodCall('prepareForVideoRecording', arguments: null), + ]); + }); + + test('Should start recording a video', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: pluginChannelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await plugin.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: pluginChannelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await plugin.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: pluginChannelName, + methods: {'stopVideoRecording': '/test/path.mp4'}, + ); + + // Act + final XFile file = await plugin.stopVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('stopVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.mp4'); + }); + + test('Should throw UnsupportedError when pause video recording is called', + () async { + // Act + expect( + () => plugin.pauseVideoRecording(cameraId), + throwsA(isA()), + ); + }); + + test( + 'Should throw UnsupportedError when resume video recording is called', + () async { + // Act + expect( + () => plugin.resumeVideoRecording(cameraId), + throwsA(isA()), + ); + }); + + test('Should throw UnimplementedError when flash mode is set', () async { + // Act + expect( + () => plugin.setFlashMode(cameraId, FlashMode.torch), + throwsA(isA()), + ); + }); + + test('Should throw UnimplementedError when exposure mode is set', + () async { + // Act + expect( + () => plugin.setExposureMode(cameraId, ExposureMode.auto), + throwsA(isA()), + ); + }); + + test('Should throw UnsupportedError when exposure point is set', + () async { + // Act + expect( + () => plugin.setExposurePoint(cameraId, null), + throwsA(isA()), + ); + }); + + test('Should get the min exposure offset', () async { + // Act + final double minExposureOffset = + await plugin.getMinExposureOffset(cameraId); + + // Assert + expect(minExposureOffset, 0.0); + }); + + test('Should get the max exposure offset', () async { + // Act + final double maxExposureOffset = + await plugin.getMaxExposureOffset(cameraId); + + // Assert + expect(maxExposureOffset, 0.0); + }); + + test('Should get the exposure offset step size', () async { + // Act + final double stepSize = + await plugin.getExposureOffsetStepSize(cameraId); + + // Assert + expect(stepSize, 1.0); + }); + + test('Should throw UnimplementedError when exposure offset is set', + () async { + // Act + expect( + () => plugin.setExposureOffset(cameraId, 0.5), + throwsA(isA()), + ); + }); + + test('Should throw UnimplementedError when focus mode is set', () async { + // Act + expect( + () => plugin.setFocusMode(cameraId, FocusMode.auto), + throwsA(isA()), + ); + }); + + test('Should throw UnsupportedError when exposure point is set', + () async { + // Act + expect( + () => plugin.setFocusMode(cameraId, FocusMode.auto), + throwsA(isA()), + ); + }); + + test('Should build a texture widget as preview widget', () async { + // Act + final Widget widget = plugin.buildPreview(cameraId); + + // Act + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should throw UnimplementedError when handling unknown method', () { + final CameraWindows plugin = CameraWindows(); + + expect( + () => plugin.handleCameraMethodCall( + const MethodCall('unknown_method'), 1), + throwsA(isA())); + }); + + test('Should get the max zoom level', () async { + // Act + final double maxZoomLevel = await plugin.getMaxZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 1.0); + }); + + test('Should get the min zoom level', () async { + // Act + final double maxZoomLevel = await plugin.getMinZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 1.0); + }); + + test('Should throw UnimplementedError when zoom level is set', () async { + // Act + expect( + () => plugin.setZoomLevel(cameraId, 2.0), + throwsA(isA()), + ); + }); + + test( + 'Should throw UnimplementedError when lock capture orientation is called', + () async { + // Act + expect( + () => plugin.setZoomLevel(cameraId, 2.0), + throwsA(isA()), + ); + }); + + test( + 'Should throw UnimplementedError when unlock capture orientation is called', + () async { + // Act + expect( + () => plugin.unlockCaptureOrientation(cameraId), + throwsA(isA()), + ); + }); + + test('Should pause the camera preview', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: pluginChannelName, + methods: {'pausePreview': null}, + ); + + // Act + await plugin.pausePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pausePreview', + arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should resume the camera preview', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: pluginChannelName, + methods: {'resumePreview': null}, + ); + + // Act + await plugin.resumePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumePreview', + arguments: {'cameraId': cameraId}), + ]); + }); + }); + }); +} diff --git a/packages/camera/camera_windows/test/utils/method_channel_mock.dart b/packages/camera/camera_windows/test/utils/method_channel_mock.dart new file mode 100644 index 000000000000..22f7ecead589 --- /dev/null +++ b/packages/camera/camera_windows/test/utils/method_channel_mock.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/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// A mock [MethodChannel] implementation for use in tests. +class MethodChannelMock { + /// Creates a new instance with the specified channel name. + /// + /// This method channel will handle all method invocations specified by + /// returning the value mapped to the method name key. If a delay is + /// specified, results are returned after the delay has elapsed. + 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 TEST 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_windows/windows/.gitignore b/packages/camera/camera_windows/windows/.gitignore new file mode 100644 index 000000000000..b3eb2be169a5 --- /dev/null +++ b/packages/camera/camera_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# 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/camera/camera_windows/windows/CMakeLists.txt b/packages/camera/camera_windows/windows/CMakeLists.txt new file mode 100644 index 000000000000..caeb1095f5a5 --- /dev/null +++ b/packages/camera/camera_windows/windows/CMakeLists.txt @@ -0,0 +1,99 @@ +cmake_minimum_required(VERSION 3.14) +set(PROJECT_NAME "camera_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +list(APPEND PLUGIN_SOURCES + "camera_plugin.h" + "camera_plugin.cpp" + "camera.h" + "camera.cpp" + "capture_controller.h" + "capture_controller.cpp" + "capture_controller_listener.h" + "capture_engine_listener.h" + "capture_engine_listener.cpp" + "string_utils.h" + "string_utils.cpp" + "capture_device_info.h" + "capture_device_info.cpp" + "preview_handler.h" + "preview_handler.cpp" + "record_handler.h" + "record_handler.cpp" + "photo_handler.h" + "photo_handler.cpp" + "texture_handler.h" + "texture_handler.cpp" + "com_heap_ptr.h" +) + +add_library(${PLUGIN_NAME} SHARED + "camera_windows.cpp" + "include/camera_windows/camera_windows.h" + ${PLUGIN_SOURCES} +) + +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 flutter_wrapper_plugin) +target_link_libraries(${PLUGIN_NAME} PRIVATE mf mfplat mfuuid d3d11) + +# List of absolute paths to libraries that should be bundled with the plugin +set(camera_windows_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. +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 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/camera_plugin_test.cpp + test/camera_test.cpp + test/capture_controller_test.cpp + ${PLUGIN_SOURCES} +) +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 mf mfplat mfuuid d3d11) +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/camera/camera_windows/windows/camera.cpp b/packages/camera/camera_windows/windows/camera.cpp new file mode 100644 index 000000000000..c21f8ab0af78 --- /dev/null +++ b/packages/camera/camera_windows/windows/camera.cpp @@ -0,0 +1,264 @@ +// Copyright 2013 The Flutter 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 "camera.h" + +namespace camera_windows { +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +// Camera channel events. +constexpr char kCameraMethodChannelBaseName[] = + "plugins.flutter.io/camera_windows/camera"; +constexpr char kVideoRecordedEvent[] = "video_recorded"; +constexpr char kCameraClosingEvent[] = "camera_closing"; +constexpr char kErrorEvent[] = "error"; + +CameraImpl::CameraImpl(const std::string& device_id) + : device_id_(device_id), Camera(device_id) {} + +CameraImpl::~CameraImpl() { + // Sends camera closing event. + OnCameraClosing(); + + capture_controller_ = nullptr; + SendErrorForPendingResults("plugin_disposed", + "Plugin disposed before request was handled"); +} + +void 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); +} + +void CameraImpl::InitCamera( + std::unique_ptr capture_controller_factory, + flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, bool record_audio, + ResolutionPreset resolution_preset) { + assert(!device_id_.empty()); + messenger_ = messenger; + capture_controller_ = + capture_controller_factory->CreateCaptureController(this); + capture_controller_->InitCaptureDevice(texture_registrar, device_id_, + record_audio, resolution_preset); +} + +bool CameraImpl::AddPendingResult( + PendingResultType type, std::unique_ptr> result) { + assert(result); + + auto it = pending_results_.find(type); + if (it != pending_results_.end()) { + result->Error("Duplicate request", "Method handler already called"); + return false; + } + + pending_results_.insert(std::make_pair(type, std::move(result))); + return true; +} + +std::unique_ptr> CameraImpl::GetPendingResultByType( + PendingResultType type) { + auto it = pending_results_.find(type); + if (it == pending_results_.end()) { + return nullptr; + } + auto result = std::move(it->second); + pending_results_.erase(it); + return result; +} + +bool CameraImpl::HasPendingResultByType(PendingResultType type) const { + auto it = pending_results_.find(type); + if (it == pending_results_.end()) { + return false; + } + return it->second != nullptr; +} + +void CameraImpl::SendErrorForPendingResults(const std::string& error_code, + const std::string& descripion) { + for (const auto& pending_result : pending_results_) { + pending_result.second->Error(error_code, descripion); + } + pending_results_.clear(); +} + +MethodChannel<>* CameraImpl::GetMethodChannel() { + assert(messenger_); + assert(camera_id_); + + // Use existing channel if initialized + if (camera_channel_) { + return camera_channel_.get(); + } + + auto channel_name = + std::string(kCameraMethodChannelBaseName) + std::to_string(camera_id_); + + camera_channel_ = std::make_unique>( + messenger_, channel_name, &flutter::StandardMethodCodec::GetInstance()); + + return camera_channel_.get(); +} + +void CameraImpl::OnCreateCaptureEngineSucceeded(int64_t texture_id) { + // Use texture id as camera id + camera_id_ = texture_id; + auto pending_result = + GetPendingResultByType(PendingResultType::kCreateCamera); + if (pending_result) { + pending_result->Success(EncodableMap( + {{EncodableValue("cameraId"), EncodableValue(texture_id)}})); + } +} + +void CameraImpl::OnCreateCaptureEngineFailed(const std::string& error) { + auto pending_result = + GetPendingResultByType(PendingResultType::kCreateCamera); + if (pending_result) { + pending_result->Error("camera_error", error); + } +} + +void CameraImpl::OnStartPreviewSucceeded(int32_t width, int32_t height) { + auto pending_result = GetPendingResultByType(PendingResultType::kInitialize); + if (pending_result) { + pending_result->Success(EncodableValue(EncodableMap({ + {EncodableValue("previewWidth"), + EncodableValue(static_cast(width))}, + {EncodableValue("previewHeight"), + EncodableValue(static_cast(height))}, + }))); + } +}; + +void CameraImpl::OnStartPreviewFailed(const std::string& error) { + auto pending_result = GetPendingResultByType(PendingResultType::kInitialize); + if (pending_result) { + pending_result->Error("camera_error", error); + } +}; + +void CameraImpl::OnResumePreviewSucceeded() { + auto pending_result = + GetPendingResultByType(PendingResultType::kResumePreview); + if (pending_result) { + pending_result->Success(); + } +} + +void CameraImpl::OnResumePreviewFailed(const std::string& error) { + auto pending_result = + GetPendingResultByType(PendingResultType::kResumePreview); + if (pending_result) { + pending_result->Error("camera_error", error); + } +} + +void CameraImpl::OnPausePreviewSucceeded() { + auto pending_result = + GetPendingResultByType(PendingResultType::kPausePreview); + if (pending_result) { + pending_result->Success(); + } +} + +void CameraImpl::OnPausePreviewFailed(const std::string& error) { + auto pending_result = + GetPendingResultByType(PendingResultType::kPausePreview); + if (pending_result) { + pending_result->Error("camera_error", error); + } +} + +void CameraImpl::OnStartRecordSucceeded() { + auto pending_result = GetPendingResultByType(PendingResultType::kStartRecord); + if (pending_result) { + pending_result->Success(); + } +}; + +void CameraImpl::OnStartRecordFailed(const std::string& error) { + auto pending_result = GetPendingResultByType(PendingResultType::kStartRecord); + if (pending_result) { + pending_result->Error("camera_error", error); + } +}; + +void CameraImpl::OnStopRecordSucceeded(const std::string& file_path) { + auto pending_result = GetPendingResultByType(PendingResultType::kStopRecord); + if (pending_result) { + pending_result->Success(EncodableValue(file_path)); + } +}; + +void CameraImpl::OnStopRecordFailed(const std::string& error) { + auto pending_result = GetPendingResultByType(PendingResultType::kStopRecord); + if (pending_result) { + pending_result->Error("camera_error", error); + } +}; + +void CameraImpl::OnTakePictureSucceeded(const std::string& file_path) { + auto pending_result = GetPendingResultByType(PendingResultType::kTakePicture); + if (pending_result) { + pending_result->Success(EncodableValue(file_path)); + } +}; + +void CameraImpl::OnTakePictureFailed(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); + } +}; + +void CameraImpl::OnVideoRecordSucceeded(const std::string& file_path, + int64_t video_duration_ms) { + if (messenger_ && camera_id_ >= 0) { + auto channel = GetMethodChannel(); + + std::unique_ptr message_data = + std::make_unique( + EncodableMap({{EncodableValue("path"), EncodableValue(file_path)}, + {EncodableValue("maxVideoDuration"), + EncodableValue(video_duration_ms)}})); + + channel->InvokeMethod(kVideoRecordedEvent, std::move(message_data)); + } +} + +void CameraImpl::OnVideoRecordFailed(const std::string& error){}; + +void CameraImpl::OnCaptureError(const std::string& error) { + if (messenger_ && camera_id_ >= 0) { + auto channel = GetMethodChannel(); + + std::unique_ptr message_data = + std::make_unique(EncodableMap( + {{EncodableValue("description"), EncodableValue(error)}})); + channel->InvokeMethod(kErrorEvent, std::move(message_data)); + } + + SendErrorForPendingResults("capture_error", error); +} + +void CameraImpl::OnCameraClosing() { + if (messenger_ && camera_id_ >= 0) { + auto channel = GetMethodChannel(); + channel->InvokeMethod(kCameraClosingEvent, + std::move(std::make_unique())); + } +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/camera.h b/packages/camera/camera_windows/windows/camera.h new file mode 100644 index 000000000000..6996231c7ab4 --- /dev/null +++ b/packages/camera/camera_windows/windows/camera.h @@ -0,0 +1,194 @@ +// Copyright 2013 The Flutter 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_CAMERA_CAMERA_WINDOWS_WINDOWS_CAMERA_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAMERA_H_ + +#include +#include + +#include + +#include "capture_controller.h" + +namespace camera_windows { + +using flutter::EncodableMap; +using flutter::MethodChannel; +using flutter::MethodResult; + +// A set of result types that are stored +// for processing asynchronous commands. +enum class PendingResultType { + kCreateCamera, + kInitialize, + kTakePicture, + kStartRecord, + kStopRecord, + kPausePreview, + kResumePreview, +}; + +// Interface implemented by cameras. +// +// Access is provided to an associated |CaptureController|, which can be used +// to capture video or photo from the camera. +class Camera : public CaptureControllerListener { + public: + explicit Camera(const std::string& device_id) {} + virtual ~Camera() = default; + + // Disallow copy and move. + Camera(const Camera&) = delete; + Camera& operator=(const Camera&) = delete; + + // Tests if this camera has the specified device ID. + virtual bool HasDeviceId(std::string& device_id) const = 0; + + // Tests if this camera has the specified camera ID. + virtual bool HasCameraId(int64_t camera_id) const = 0; + + // Adds a pending result. + // + // Returns an error result if the result has already been added. + virtual bool AddPendingResult(PendingResultType type, + std::unique_ptr> result) = 0; + + // Checks if a pending result of the specified type already exists. + virtual bool HasPendingResultByType(PendingResultType type) const = 0; + + // Returns a |CaptureController| that allows capturing video or still photos + // from this camera. + virtual camera_windows::CaptureController* GetCaptureController() = 0; + + // Initializes this camera and its associated capture controller. + virtual void InitCamera(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, + bool record_audio, + ResolutionPreset resolution_preset) = 0; +}; + +// Concrete implementation of the |Camera| interface. +// +// This implementation is responsible for initializing the capture controller, +// listening for camera events, processing pending results, and notifying +// application code of processed events via the method channel. +class CameraImpl : public Camera { + public: + explicit CameraImpl(const std::string& device_id); + virtual ~CameraImpl(); + + // Disallow copy and move. + CameraImpl(const CameraImpl&) = delete; + CameraImpl& operator=(const CameraImpl&) = delete; + + // CaptureControllerListener + void OnCreateCaptureEngineSucceeded(int64_t texture_id) override; + void OnCreateCaptureEngineFailed(const std::string& error) override; + void OnStartPreviewSucceeded(int32_t width, int32_t height) override; + void OnStartPreviewFailed(const std::string& error) override; + void OnPausePreviewSucceeded() override; + void OnPausePreviewFailed(const std::string& error) override; + void OnResumePreviewSucceeded() override; + void OnResumePreviewFailed(const std::string& error) override; + void OnStartRecordSucceeded() override; + void OnStartRecordFailed(const std::string& error) override; + void OnStopRecordSucceeded(const std::string& file_path) override; + void OnStopRecordFailed(const std::string& error) override; + void OnTakePictureSucceeded(const std::string& file_path) override; + void OnTakePictureFailed(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; + + // Camera + bool HasDeviceId(std::string& device_id) const override { + return device_id_ == device_id; + } + bool HasCameraId(int64_t camera_id) const override { + return camera_id_ == camera_id; + } + bool AddPendingResult(PendingResultType type, + std::unique_ptr> result) override; + bool HasPendingResultByType(PendingResultType type) const override; + camera_windows::CaptureController* GetCaptureController() override { + return capture_controller_.get(); + } + void InitCamera(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, bool record_audio, + ResolutionPreset resolution_preset) override; + + // Initializes the camera and its associated capture controller. + // + // This is a convenience method called by |InitCamera| but also used in + // tests. + void InitCamera( + std::unique_ptr capture_controller_factory, + flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, bool record_audio, + ResolutionPreset resolution_preset); + + private: + // Loops through all pending results and calls their error handler with given + // 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). + void SendErrorForPendingResults(const std::string& error_code, + const std::string& descripion); + + // Called when camera is disposed. + // Sends camera closing message to the cameras method channel. + void OnCameraClosing(); + + // Initializes method channel instance and returns pointer it. + MethodChannel<>* GetMethodChannel(); + + // Finds pending result by type. + // Returns nullptr if type is not present. + std::unique_ptr> GetPendingResultByType( + PendingResultType type); + + std::map>> pending_results_; + std::unique_ptr capture_controller_; + std::unique_ptr> camera_channel_; + flutter::BinaryMessenger* messenger_ = nullptr; + int64_t camera_id_ = -1; + std::string device_id_; +}; + +// Factory class for creating |Camera| instances from a specified device ID. +class CameraFactory { + public: + CameraFactory() {} + virtual ~CameraFactory() = default; + + // Disallow copy and move. + CameraFactory(const CameraFactory&) = delete; + CameraFactory& operator=(const CameraFactory&) = delete; + + // Creates camera for given device id. + virtual std::unique_ptr CreateCamera( + const std::string& device_id) = 0; +}; + +// Concrete implementation of |CameraFactory|. +class CameraFactoryImpl : public CameraFactory { + public: + CameraFactoryImpl() {} + virtual ~CameraFactoryImpl() = default; + + // Disallow copy and move. + CameraFactoryImpl(const CameraFactoryImpl&) = delete; + CameraFactoryImpl& operator=(const CameraFactoryImpl&) = delete; + + std::unique_ptr CreateCamera(const std::string& device_id) override { + return std::make_unique(device_id); + } +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAMERA_H_ diff --git a/packages/camera/camera_windows/windows/camera_plugin.cpp b/packages/camera/camera_windows/windows/camera_plugin.cpp new file mode 100644 index 000000000000..3b795e02047a --- /dev/null +++ b/packages/camera/camera_windows/windows/camera_plugin.cpp @@ -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. + +#include "camera_plugin.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "capture_device_info.h" +#include "com_heap_ptr.h" +#include "string_utils.h" + +namespace camera_windows { +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +namespace { + +// Channel events +constexpr char kChannelName[] = "plugins.flutter.io/camera_windows"; + +constexpr char kAvailableCamerasMethod[] = "availableCameras"; +constexpr char kCreateMethod[] = "create"; +constexpr char kInitializeMethod[] = "initialize"; +constexpr char kTakePictureMethod[] = "takePicture"; +constexpr char kStartVideoRecordingMethod[] = "startVideoRecording"; +constexpr char kStopVideoRecordingMethod[] = "stopVideoRecording"; +constexpr char kPausePreview[] = "pausePreview"; +constexpr char kResumePreview[] = "resumePreview"; +constexpr char kDisposeMethod[] = "dispose"; + +constexpr char kCameraNameKey[] = "cameraName"; +constexpr char kResolutionPresetKey[] = "resolutionPreset"; +constexpr char kEnableAudioKey[] = "enableAudio"; + +constexpr char kCameraIdKey[] = "cameraId"; +constexpr char kMaxVideoDurationKey[] = "maxVideoDuration"; + +constexpr char kResolutionPresetValueLow[] = "low"; +constexpr char kResolutionPresetValueMedium[] = "medium"; +constexpr char kResolutionPresetValueHigh[] = "high"; +constexpr char kResolutionPresetValueVeryHigh[] = "veryHigh"; +constexpr char kResolutionPresetValueUltraHigh[] = "ultraHigh"; +constexpr char kResolutionPresetValueMax[] = "max"; + +const std::string kPictureCaptureExtension = "jpeg"; +const std::string kVideoCaptureExtension = "mp4"; + +// 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); +} + +// Looks for |key| in |map|, returning the associated int64 value if it is +// present, or std::nullopt if not. +std::optional GetInt64ValueOrNull(const EncodableMap& map, + const char* key) { + auto value = ValueOrNull(map, key); + if (!value) { + return std::nullopt; + } + + if (std::holds_alternative(*value)) { + return static_cast(std::get(*value)); + } + auto val64 = std::get_if(value); + if (!val64) { + return std::nullopt; + } + return *val64; +} + +// Parses resolution preset argument to enum value. +ResolutionPreset ParseResolutionPreset(const std::string& resolution_preset) { + if (resolution_preset.compare(kResolutionPresetValueLow) == 0) { + return ResolutionPreset::kLow; + } else if (resolution_preset.compare(kResolutionPresetValueMedium) == 0) { + return ResolutionPreset::kMedium; + } else if (resolution_preset.compare(kResolutionPresetValueHigh) == 0) { + return ResolutionPreset::kHigh; + } else if (resolution_preset.compare(kResolutionPresetValueVeryHigh) == 0) { + return ResolutionPreset::kVeryHigh; + } else if (resolution_preset.compare(kResolutionPresetValueUltraHigh) == 0) { + return ResolutionPreset::kUltraHigh; + } else if (resolution_preset.compare(kResolutionPresetValueMax) == 0) { + return ResolutionPreset::kMax; + } + return ResolutionPreset::kAuto; +} + +// Builds CaptureDeviceInfo object from given device holding device name and id. +std::unique_ptr GetDeviceInfo(IMFActivate* device) { + assert(device); + auto device_info = std::make_unique(); + ComHeapPtr name; + UINT32 name_size; + + HRESULT hr = device->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, + &name, &name_size); + if (FAILED(hr)) { + return device_info; + } + + ComHeapPtr id; + UINT32 id_size; + hr = device->GetAllocatedString( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, &id, &id_size); + + if (FAILED(hr)) { + return device_info; + } + + device_info->SetDisplayName(Utf8FromUtf16(std::wstring(name, name_size))); + device_info->SetDeviceID(Utf8FromUtf16(std::wstring(id, id_size))); + return device_info; +} + +// Builds datetime string from current time. +// Used as part of the filenames for captured pictures and videos. +std::string GetCurrentTimeString() { + std::chrono::system_clock::duration now = + std::chrono::system_clock::now().time_since_epoch(); + + auto s = std::chrono::duration_cast(now).count(); + auto ms = + std::chrono::duration_cast(now).count() % 1000; + + struct tm newtime; + localtime_s(&newtime, &s); + + std::string time_start = ""; + time_start.resize(80); + size_t len = + strftime(&time_start[0], time_start.size(), "%Y_%m%d_%H%M%S_", &newtime); + if (len > 0) { + time_start.resize(len); + } + + // Add milliseconds to make sure the filename is unique + return time_start + std::to_string(ms); +} + +// Builds file path for picture capture. +std::optional GetFilePathForPicture() { + ComHeapPtr known_folder_path; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_Pictures, KF_FLAG_CREATE, nullptr, + &known_folder_path); + if (FAILED(hr)) { + return std::nullopt; + } + + std::string path = Utf8FromUtf16(std::wstring(known_folder_path)); + + return path + "\\" + "PhotoCapture_" + GetCurrentTimeString() + "." + + kPictureCaptureExtension; +} + +// Builds file path for video capture. +std::optional GetFilePathForVideo() { + ComHeapPtr known_folder_path; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_Videos, KF_FLAG_CREATE, nullptr, + &known_folder_path); + if (FAILED(hr)) { + return std::nullopt; + } + + std::string path = Utf8FromUtf16(std::wstring(known_folder_path)); + + return path + "\\" + "VideoCapture_" + GetCurrentTimeString() + "." + + kVideoCaptureExtension; +} +} // namespace + +// static +void CameraPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto channel = std::make_unique>( + registrar->messenger(), kChannelName, + &flutter::StandardMethodCodec::GetInstance()); + + std::unique_ptr plugin = std::make_unique( + registrar->texture_registrar(), registrar->messenger()); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +CameraPlugin::CameraPlugin(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger) + : texture_registrar_(texture_registrar), + messenger_(messenger), + camera_factory_(std::make_unique()) {} + +CameraPlugin::CameraPlugin(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, + std::unique_ptr camera_factory) + : texture_registrar_(texture_registrar), + messenger_(messenger), + camera_factory_(std::move(camera_factory)) {} + +CameraPlugin::~CameraPlugin() {} + +void CameraPlugin::HandleMethodCall( + const flutter::MethodCall<>& method_call, + std::unique_ptr> result) { + const std::string& method_name = method_call.method_name(); + + if (method_name.compare(kAvailableCamerasMethod) == 0) { + return AvailableCamerasMethodHandler(std::move(result)); + } else if (method_name.compare(kCreateMethod) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return CreateMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kInitializeMethod) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return this->InitializeMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kTakePictureMethod) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return TakePictureMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kStartVideoRecordingMethod) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return StartVideoRecordingMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kStopVideoRecordingMethod) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return StopVideoRecordingMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kPausePreview) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return PausePreviewMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kResumePreview) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return ResumePreviewMethodHandler(*arguments, std::move(result)); + } else if (method_name.compare(kDisposeMethod) == 0) { + const auto* arguments = + std::get_if(method_call.arguments()); + assert(arguments); + + return DisposeMethodHandler(*arguments, std::move(result)); + } else { + result->NotImplemented(); + } +} + +Camera* CameraPlugin::GetCameraByDeviceId(std::string& device_id) { + for (auto it = begin(cameras_); it != end(cameras_); ++it) { + if ((*it)->HasDeviceId(device_id)) { + return it->get(); + } + } + return nullptr; +} + +Camera* CameraPlugin::GetCameraByCameraId(int64_t camera_id) { + for (auto it = begin(cameras_); it != end(cameras_); ++it) { + if ((*it)->HasCameraId(camera_id)) { + return it->get(); + } + } + return nullptr; +} + +void CameraPlugin::DisposeCameraByCameraId(int64_t camera_id) { + for (auto it = begin(cameras_); it != end(cameras_); ++it) { + if ((*it)->HasCameraId(camera_id)) { + cameras_.erase(it); + return; + } + } +} + +void CameraPlugin::AvailableCamerasMethodHandler( + std::unique_ptr> result) { + // Enumerate devices. + ComHeapPtr devices; + UINT32 count = 0; + if (!this->EnumerateVideoCaptureDeviceSources(&devices, &count)) { + result->Error("System error", "Failed to get available cameras"); + // No need to free devices here, cos allocation failed. + return; + } + + if (count == 0) { + result->Success(EncodableValue(EncodableList())); + return; + } + + // Format found devices to the response. + EncodableList devices_list; + for (UINT32 i = 0; i < count; ++i) { + auto device_info = GetDeviceInfo(devices[i]); + auto deviceName = device_info->GetUniqueDeviceName(); + + devices_list.push_back(EncodableMap({ + {EncodableValue("name"), EncodableValue(deviceName)}, + {EncodableValue("lensFacing"), EncodableValue("front")}, + {EncodableValue("sensorOrientation"), EncodableValue(0)}, + })); + } + + result->Success(std::move(EncodableValue(devices_list))); +} + +bool CameraPlugin::EnumerateVideoCaptureDeviceSources(IMFActivate*** devices, + UINT32* count) { + return CaptureControllerImpl::EnumerateVideoCaptureDeviceSources(devices, + count); +} + +void CameraPlugin::CreateMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + // Parse enableAudio argument. + const auto* record_audio = + std::get_if(ValueOrNull(args, kEnableAudioKey)); + if (!record_audio) { + return result->Error("argument_error", + std::string(kEnableAudioKey) + " argument missing"); + } + + // Parse cameraName argument. + const auto* camera_name = + std::get_if(ValueOrNull(args, kCameraNameKey)); + if (!camera_name) { + return result->Error("argument_error", + std::string(kCameraNameKey) + " argument missing"); + } + + auto device_info = std::make_unique(); + if (!device_info->ParseDeviceInfoFromCameraName(*camera_name)) { + return result->Error( + "camera_error", "Cannot parse argument " + std::string(kCameraNameKey)); + } + + auto device_id = device_info->GetDeviceId(); + if (GetCameraByDeviceId(device_id)) { + return result->Error("camera_error", + "Camera with given device id already exists. Existing " + "camera must be disposed before creating it again."); + } + + std::unique_ptr camera = + camera_factory_->CreateCamera(device_id); + + if (camera->HasPendingResultByType(PendingResultType::kCreateCamera)) { + return result->Error("camera_error", + "Pending camera creation request exists"); + } + + if (camera->AddPendingResult(PendingResultType::kCreateCamera, + std::move(result))) { + // Parse resolution preset argument. + const auto* resolution_preset_argument = + std::get_if(ValueOrNull(args, kResolutionPresetKey)); + ResolutionPreset resolution_preset; + if (resolution_preset_argument) { + resolution_preset = ParseResolutionPreset(*resolution_preset_argument); + } else { + resolution_preset = ResolutionPreset::kAuto; + } + + camera->InitCamera(texture_registrar_, messenger_, *record_audio, + resolution_preset); + cameras_.push_back(std::move(camera)); + } +} + +void CameraPlugin::InitializeMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + auto camera = GetCameraByCameraId(*camera_id); + if (!camera) { + return result->Error("camera_error", "Camera not created"); + } + + if (camera->HasPendingResultByType(PendingResultType::kInitialize)) { + return result->Error("camera_error", + "Pending initialization request exists"); + } + + if (camera->AddPendingResult(PendingResultType::kInitialize, + std::move(result))) { + auto cc = camera->GetCaptureController(); + assert(cc); + cc->StartPreview(); + } +} + +void CameraPlugin::PausePreviewMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + auto camera = GetCameraByCameraId(*camera_id); + if (!camera) { + return result->Error("camera_error", "Camera not created"); + } + + if (camera->HasPendingResultByType(PendingResultType::kPausePreview)) { + return result->Error("camera_error", + "Pending pause preview request exists"); + } + + if (camera->AddPendingResult(PendingResultType::kPausePreview, + std::move(result))) { + auto cc = camera->GetCaptureController(); + assert(cc); + cc->PausePreview(); + } +} + +void CameraPlugin::ResumePreviewMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + auto camera = GetCameraByCameraId(*camera_id); + if (!camera) { + return result->Error("camera_error", "Camera not created"); + } + + if (camera->HasPendingResultByType(PendingResultType::kResumePreview)) { + return result->Error("camera_error", + "Pending resume preview request exists"); + } + + if (camera->AddPendingResult(PendingResultType::kResumePreview, + std::move(result))) { + auto cc = camera->GetCaptureController(); + assert(cc); + cc->ResumePreview(); + } +} + +void CameraPlugin::StartVideoRecordingMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + auto camera = GetCameraByCameraId(*camera_id); + if (!camera) { + return result->Error("camera_error", "Camera not created"); + } + + if (camera->HasPendingResultByType(PendingResultType::kStartRecord)) { + return result->Error("camera_error", + "Pending start recording request exists"); + } + + int64_t max_video_duration_ms = -1; + auto requested_max_video_duration_ms = + std::get_if(ValueOrNull(args, kMaxVideoDurationKey)); + + if (requested_max_video_duration_ms != nullptr) { + max_video_duration_ms = *requested_max_video_duration_ms; + } + + std::optional path = GetFilePathForVideo(); + if (path) { + if (camera->AddPendingResult(PendingResultType::kStartRecord, + std::move(result))) { + auto cc = camera->GetCaptureController(); + assert(cc); + cc->StartRecord(*path, max_video_duration_ms); + } + } else { + return result->Error("system_error", + "Failed to get path for video capture"); + } +} + +void CameraPlugin::StopVideoRecordingMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + auto camera = GetCameraByCameraId(*camera_id); + if (!camera) { + return result->Error("camera_error", "Camera not created"); + } + + if (camera->HasPendingResultByType(PendingResultType::kStopRecord)) { + return result->Error("camera_error", + "Pending stop recording request exists"); + } + + if (camera->AddPendingResult(PendingResultType::kStopRecord, + std::move(result))) { + auto cc = camera->GetCaptureController(); + assert(cc); + cc->StopRecord(); + } +} + +void CameraPlugin::TakePictureMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + auto camera = GetCameraByCameraId(*camera_id); + if (!camera) { + return result->Error("camera_error", "Camera not created"); + } + + if (camera->HasPendingResultByType(PendingResultType::kTakePicture)) { + return result->Error("camera_error", "Pending take picture request exists"); + } + + std::optional path = GetFilePathForPicture(); + if (path) { + if (camera->AddPendingResult(PendingResultType::kTakePicture, + std::move(result))) { + auto cc = camera->GetCaptureController(); + assert(cc); + cc->TakePicture(*path); + } + } else { + return result->Error("system_error", + "Failed to get capture path for picture"); + } +} + +void CameraPlugin::DisposeMethodHandler( + const EncodableMap& args, std::unique_ptr> result) { + auto camera_id = GetInt64ValueOrNull(args, kCameraIdKey); + if (!camera_id) { + return result->Error("argument_error", + std::string(kCameraIdKey) + " missing"); + } + + DisposeCameraByCameraId(*camera_id); + result->Success(); +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/camera_plugin.h b/packages/camera/camera_windows/windows/camera_plugin.h new file mode 100644 index 000000000000..1baa2477beb5 --- /dev/null +++ b/packages/camera/camera_windows/windows/camera_plugin.h @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAMERA_PLUGIN_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAMERA_PLUGIN_H_ + +#include +#include +#include +#include + +#include + +#include "camera.h" +#include "capture_controller.h" +#include "capture_controller_listener.h" + +namespace camera_windows { +using flutter::MethodResult; + +namespace test { +namespace { +// Forward declaration of test class. +class MockCameraPlugin; +} // namespace +} // namespace test + +class CameraPlugin : public flutter::Plugin, + public VideoCaptureDeviceEnumerator { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + CameraPlugin(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger); + + // Creates a plugin instance with the given CameraFactory instance. + // Exists for unit testing with mock implementations. + CameraPlugin(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, + std::unique_ptr camera_factory); + + virtual ~CameraPlugin(); + + // Disallow copy and move. + CameraPlugin(const CameraPlugin&) = delete; + CameraPlugin& operator=(const CameraPlugin&) = delete; + + // Called when a method is called on plugin channel. + void HandleMethodCall(const flutter::MethodCall<>& method_call, + std::unique_ptr> result); + + private: + // Loops through cameras and returns camera + // with matching device_id or nullptr. + Camera* GetCameraByDeviceId(std::string& device_id); + + // Loops through cameras and returns camera + // with matching camera_id or nullptr. + Camera* GetCameraByCameraId(int64_t camera_id); + + // Disposes camera by camera id. + void DisposeCameraByCameraId(int64_t camera_id); + + // Enumerates video capture devices. + bool EnumerateVideoCaptureDeviceSources(IMFActivate*** devices, + UINT32* count) override; + + // Handles availableCameras method calls. + // Enumerates video capture devices and + // returns list of available camera devices. + void AvailableCamerasMethodHandler( + std::unique_ptr> result); + + // Handles create method calls. + // Creates camera and initializes capture controller for requested device. + // Stores result object to be handled after request is processed. + void CreateMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles initialize method calls. + // Requests existing camera controller to start preview. + // Stores result object to be handled after request is processed. + void InitializeMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles takePicture method calls. + // Requests existing camera controller to take photo. + // Stores result object to be handled after request is processed. + void TakePictureMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles startVideoRecording method calls. + // Requests existing camera controller to start recording. + // Stores result object to be handled after request is processed. + void StartVideoRecordingMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles stopVideoRecording method calls. + // Requests existing camera controller to stop recording. + // Stores result object to be handled after request is processed. + void StopVideoRecordingMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles pausePreview method calls. + // Requests existing camera controller to pause recording. + // Stores result object to be handled after request is processed. + void PausePreviewMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles resumePreview method calls. + // Requests existing camera controller to resume preview. + // Stores result object to be handled after request is processed. + void ResumePreviewMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + // Handles dsipose method calls. + // Disposes camera if exists. + void DisposeMethodHandler(const EncodableMap& args, + std::unique_ptr> result); + + std::unique_ptr camera_factory_; + flutter::TextureRegistrar* texture_registrar_; + flutter::BinaryMessenger* messenger_; + std::vector> cameras_; + + friend class camera_windows::test::MockCameraPlugin; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAMERA_PLUGIN_H_ diff --git a/packages/camera/camera_windows/windows/camera_windows.cpp b/packages/camera/camera_windows/windows/camera_windows.cpp new file mode 100644 index 000000000000..2d6b781af59f --- /dev/null +++ b/packages/camera/camera_windows/windows/camera_windows.cpp @@ -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 "include/camera_windows/camera_windows.h" + +#include + +#include "camera_plugin.h" + +void CameraWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + camera_windows::CameraPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/camera/camera_windows/windows/capture_controller.cpp b/packages/camera/camera_windows/windows/capture_controller.cpp new file mode 100644 index 000000000000..084b03640bef --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_controller.cpp @@ -0,0 +1,861 @@ +// Copyright 2013 The Flutter 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 "capture_controller.h" + +#include +#include +#include + +#include +#include + +#include "com_heap_ptr.h" +#include "photo_handler.h" +#include "preview_handler.h" +#include "record_handler.h" +#include "string_utils.h" +#include "texture_handler.h" + +namespace camera_windows { + +using Microsoft::WRL::ComPtr; + +CaptureControllerImpl::CaptureControllerImpl( + CaptureControllerListener* listener) + : capture_controller_listener_(listener), CaptureController(){}; + +CaptureControllerImpl::~CaptureControllerImpl() { + ResetCaptureController(); + capture_controller_listener_ = nullptr; +}; + +// static +bool CaptureControllerImpl::EnumerateVideoCaptureDeviceSources( + IMFActivate*** devices, UINT32* count) { + ComPtr attributes; + + HRESULT hr = MFCreateAttributes(&attributes, 1); + if (FAILED(hr)) { + return false; + } + + hr = attributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (FAILED(hr)) { + return false; + } + + hr = MFEnumDeviceSources(attributes.Get(), devices, count); + if (FAILED(hr)) { + return false; + } + + return true; +} + +HRESULT CaptureControllerImpl::CreateDefaultAudioCaptureSource() { + audio_source_ = nullptr; + ComHeapPtr devices; + UINT32 count = 0; + + ComPtr attributes; + HRESULT hr = MFCreateAttributes(&attributes, 1); + + if (SUCCEEDED(hr)) { + hr = attributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_GUID); + } + + if (SUCCEEDED(hr)) { + hr = MFEnumDeviceSources(attributes.Get(), &devices, &count); + } + + if (SUCCEEDED(hr) && count > 0) { + ComHeapPtr audio_device_id; + UINT32 audio_device_id_size; + + // Use first audio device. + hr = devices[0]->GetAllocatedString( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_ENDPOINT_ID, &audio_device_id, + &audio_device_id_size); + + if (SUCCEEDED(hr)) { + ComPtr audio_capture_source_attributes; + hr = MFCreateAttributes(&audio_capture_source_attributes, 2); + + if (SUCCEEDED(hr)) { + hr = audio_capture_source_attributes->SetGUID( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_GUID); + } + + if (SUCCEEDED(hr)) { + hr = audio_capture_source_attributes->SetString( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_ENDPOINT_ID, + audio_device_id); + } + + if (SUCCEEDED(hr)) { + hr = MFCreateDeviceSource(audio_capture_source_attributes.Get(), + audio_source_.GetAddressOf()); + } + } + } + + return hr; +} + +HRESULT CaptureControllerImpl::CreateVideoCaptureSourceForDevice( + const std::string& video_device_id) { + video_source_ = nullptr; + + ComPtr video_capture_source_attributes; + + HRESULT hr = MFCreateAttributes(&video_capture_source_attributes, 2); + if (FAILED(hr)) { + return hr; + } + + hr = video_capture_source_attributes->SetGUID( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (FAILED(hr)) { + return hr; + } + + hr = video_capture_source_attributes->SetString( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, + Utf16FromUtf8(video_device_id).c_str()); + if (FAILED(hr)) { + return hr; + } + + hr = MFCreateDeviceSource(video_capture_source_attributes.Get(), + video_source_.GetAddressOf()); + return hr; +} + +HRESULT CaptureControllerImpl::CreateD3DManagerWithDX11Device() { + // TODO: Use existing ANGLE device + + HRESULT hr = S_OK; + hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, nullptr, 0, + D3D11_SDK_VERSION, &dx11_device_, nullptr, nullptr); + if (FAILED(hr)) { + return hr; + } + + // Enable multithread protection + ComPtr multi_thread; + hr = dx11_device_.As(&multi_thread); + if (FAILED(hr)) { + return hr; + } + + multi_thread->SetMultithreadProtected(TRUE); + + hr = MFCreateDXGIDeviceManager(&dx_device_reset_token_, + dxgi_device_manager_.GetAddressOf()); + if (FAILED(hr)) { + return hr; + } + + hr = dxgi_device_manager_->ResetDevice(dx11_device_.Get(), + dx_device_reset_token_); + return hr; +} + +HRESULT CaptureControllerImpl::CreateCaptureEngine() { + assert(!video_device_id_.empty()); + + HRESULT hr = S_OK; + ComPtr attributes; + + // Creates capture engine only if not already initialized by test framework + if (!capture_engine_) { + ComPtr capture_engine_factory; + + hr = CoCreateInstance(CLSID_MFCaptureEngineClassFactory, nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&capture_engine_factory)); + if (FAILED(hr)) { + return hr; + } + + // Creates CaptureEngine. + hr = capture_engine_factory->CreateInstance(CLSID_MFCaptureEngine, + IID_PPV_ARGS(&capture_engine_)); + if (FAILED(hr)) { + return hr; + } + } + + hr = CreateD3DManagerWithDX11Device(); + + if (FAILED(hr)) { + return hr; + } + + // Creates video source only if not already initialized by test framework + if (!video_source_) { + hr = CreateVideoCaptureSourceForDevice(video_device_id_); + if (FAILED(hr)) { + return hr; + } + } + + // Creates audio source only if not already initialized by test framework + if (record_audio_ && !audio_source_) { + hr = CreateDefaultAudioCaptureSource(); + if (FAILED(hr)) { + return hr; + } + } + + if (!capture_engine_callback_handler_) { + capture_engine_callback_handler_ = + ComPtr(new CaptureEngineListener(this)); + } + + hr = MFCreateAttributes(&attributes, 2); + if (FAILED(hr)) { + return hr; + } + + hr = attributes->SetUnknown(MF_CAPTURE_ENGINE_D3D_MANAGER, + dxgi_device_manager_.Get()); + if (FAILED(hr)) { + return hr; + } + + hr = attributes->SetUINT32(MF_CAPTURE_ENGINE_USE_VIDEO_DEVICE_ONLY, + !record_audio_); + if (FAILED(hr)) { + return hr; + } + + hr = capture_engine_->Initialize(capture_engine_callback_handler_.Get(), + attributes.Get(), audio_source_.Get(), + video_source_.Get()); + return hr; +} + +void CaptureControllerImpl::ResetCaptureController() { + if (record_handler_) { + if (record_handler_->IsContinuousRecording()) { + StopRecord(); + } else if (record_handler_->IsTimedRecording()) { + StopTimedRecord(); + } + } + + if (preview_handler_) { + StopPreview(); + } + + // Shuts down the media foundation platform object. + // Releases all resources including threads. + // Application should call MFShutdown the same number of times as MFStartup + if (media_foundation_started_) { + MFShutdown(); + } + + // States + media_foundation_started_ = false; + capture_engine_state_ = CaptureEngineState::kNotInitialized; + preview_frame_width_ = 0; + preview_frame_height_ = 0; + capture_engine_callback_handler_ = nullptr; + capture_engine_ = nullptr; + audio_source_ = nullptr; + video_source_ = nullptr; + base_preview_media_type_ = nullptr; + base_capture_media_type_ = nullptr; + + if (dxgi_device_manager_) { + dxgi_device_manager_->ResetDevice(dx11_device_.Get(), + dx_device_reset_token_); + } + dxgi_device_manager_ = nullptr; + dx11_device_ = nullptr; + + record_handler_ = nullptr; + preview_handler_ = nullptr; + photo_handler_ = nullptr; + texture_handler_ = nullptr; +} + +void 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"); + } else if (capture_engine_state_ == CaptureEngineState::kInitializing) { + return capture_controller_listener_->OnCreateCaptureEngineFailed( + "Capture device already initializing"); + } + + capture_engine_state_ = CaptureEngineState::kInitializing; + resolution_preset_ = resolution_preset; + record_audio_ = record_audio; + texture_registrar_ = texture_registrar; + video_device_id_ = device_id; + + // MFStartup must be called before using Media Foundation. + if (!media_foundation_started_) { + HRESULT hr = MFStartup(MF_VERSION); + + if (FAILED(hr)) { + capture_controller_listener_->OnCreateCaptureEngineFailed( + "Failed to create camera"); + ResetCaptureController(); + return; + } + + media_foundation_started_ = true; + } + + HRESULT hr = CreateCaptureEngine(); + if (FAILED(hr)) { + capture_controller_listener_->OnCreateCaptureEngineFailed( + "Failed to create camera"); + ResetCaptureController(); + return; + } +} + +void CaptureControllerImpl::TakePicture(const std::string& file_path) { + assert(capture_engine_callback_handler_); + assert(capture_engine_); + + if (!IsInitialized()) { + return OnPicture(false, "Not initialized"); + } + + 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"); + } + } + + if (!photo_handler_) { + photo_handler_ = std::make_unique(); + } else if (photo_handler_->IsTakingPhoto()) { + return OnPicture(false, "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())) { + // Destroy photo handler on error cases to make sure state is resetted. + photo_handler_ = nullptr; + return OnPicture(false, "Failed to take photo"); + } +} + +uint32_t CaptureControllerImpl::GetMaxPreviewHeight() const { + switch (resolution_preset_) { + case ResolutionPreset::kLow: + return 240; + break; + case ResolutionPreset::kMedium: + return 480; + break; + case ResolutionPreset::kHigh: + return 720; + break; + case ResolutionPreset::kVeryHigh: + return 1080; + break; + case ResolutionPreset::kUltraHigh: + return 2160; + break; + case ResolutionPreset::kMax: + case ResolutionPreset::kAuto: + default: + // no limit. + return 0xffffffff; + break; + } +} + +// Finds best mediat 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, + uint32_t* target_frame_height, + float minimum_accepted_framerate = 15.f) { + assert(source); + ComPtr media_type; + + uint32_t best_width = 0; + uint32_t best_height = 0; + float best_framerate = 0.f; + + // Loop native media types. + for (int i = 0;; i++) { + if (FAILED(source->GetAvailableDeviceMediaType( + source_stream_index, i, media_type.GetAddressOf()))) { + break; + } + + uint32_t frame_rate_numerator, frame_rate_denominator; + if (FAILED(MFGetAttributeRatio(media_type.Get(), MF_MT_FRAME_RATE, + &frame_rate_numerator, + &frame_rate_denominator)) || + !frame_rate_denominator) { + continue; + } + + float frame_rate = + static_cast(frame_rate_numerator) / frame_rate_denominator; + if (frame_rate < minimum_accepted_framerate) { + continue; + } + + uint32_t frame_width; + uint32_t frame_height; + if (SUCCEEDED(MFGetAttributeSize(media_type.Get(), MF_MT_FRAME_SIZE, + &frame_width, &frame_height))) { + // Update target mediatype + if (frame_height <= max_height && + (best_width < frame_width || best_height < frame_height || + best_framerate < frame_rate)) { + media_type.CopyTo(target_media_type); + best_width = frame_width; + best_height = frame_height; + best_framerate = frame_rate; + } + } + } + + if (target_frame_width && target_frame_height) { + *target_frame_width = best_width; + *target_frame_height = best_height; + } + + return *target_media_type != nullptr; +} + +HRESULT CaptureControllerImpl::FindBaseMediaTypes() { + if (!IsInitialized()) { + return E_FAIL; + } + + ComPtr source; + HRESULT hr = capture_engine_->GetSource(&source); + if (FAILED(hr)) { + return hr; + } + + // Find base media type for previewing. + if (!FindBestMediaType( + (DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_PREVIEW, + source.Get(), base_preview_media_type_.GetAddressOf(), + GetMaxPreviewHeight(), &preview_frame_width_, + &preview_frame_height_)) { + return E_FAIL; + } + + // Find base media type for record and photo capture. + if (!FindBestMediaType( + (DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_RECORD, + source.Get(), base_capture_media_type_.GetAddressOf(), 0xffffffff, + nullptr, nullptr)) { + return E_FAIL; + } + + return S_OK; +} + +void CaptureControllerImpl::StartRecord(const std::string& file_path, + int64_t max_video_duration_ms) { + assert(capture_engine_); + + if (!IsInitialized()) { + return OnRecordStarted(false, + "Camera not initialized. Camera should be " + "disposed and reinitialized."); + } + + 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"); + } + } + + if (!record_handler_) { + record_handler_ = std::make_unique(record_audio_); + } else if (!record_handler_->CanStart()) { + return OnRecordStarted( + false, + "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, + capture_engine_.Get(), + base_capture_media_type_.Get())) { + // Destroy record handler on error cases to make sure state is resetted. + record_handler_ = nullptr; + return OnRecordStarted(false, "Failed to start video recording"); + } +} + +void CaptureControllerImpl::StopRecord() { + assert(capture_controller_listener_); + + if (!IsInitialized()) { + return OnRecordStopped(false, + "Camera not initialized. Camera should be " + "disposed and reinitialized."); + } + + if (!record_handler_ && !record_handler_->CanStop()) { + return OnRecordStopped(false, "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"); + } +} + +// Stops timed recording. Called internally when requested time is passed. +// Check MF_CAPTURE_ENGINE_RECORD_STOPPED event handling for response process. +void CaptureControllerImpl::StopTimedRecord() { + assert(capture_controller_listener_); + if (!record_handler_ || !record_handler_->IsTimedRecording()) { + return; + } + + if (!record_handler_->StopRecord(capture_engine_.Get())) { + // Destroy record handler on error cases to make sure state is resetted. + record_handler_ = nullptr; + return capture_controller_listener_->OnVideoRecordFailed( + "Failed to record video"); + } +} + +// Starts capturing preview frames using preview handler +// After first frame is captured, OnPreviewStarted is called +void CaptureControllerImpl::StartPreview() { + assert(capture_engine_callback_handler_); + assert(capture_engine_); + assert(texture_handler_); + + if (!IsInitialized() || !texture_handler_) { + return OnPreviewStarted(false, + "Camera not initialized. Camera should be " + "disposed and reinitialized."); + } + + 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"); + } + } + + texture_handler_->UpdateTextureSize(preview_frame_width_, + preview_frame_height_); + + if (!preview_handler_) { + preview_handler_ = std::make_unique(); + } else if (preview_handler_->IsInitialized()) { + return OnPreviewStarted(true, ""); + } else { + return OnPreviewStarted(false, "Preview already exists"); + } + + // Check MF_CAPTURE_ENGINE_PREVIEW_STARTED event handling for response + // process. + if (!preview_handler_->StartPreview(capture_engine_.Get(), + base_preview_media_type_.Get(), + capture_engine_callback_handler_.Get())) { + // Destroy preview handler on error cases to make sure state is resetted. + preview_handler_ = nullptr; + return OnPreviewStarted(false, "Failed to start video preview"); + } +} + +// Stops preview. Called by destructor +// Use PausePreview and ResumePreview methods to for +// pausing and resuming the preview. +// Check MF_CAPTURE_ENGINE_PREVIEW_STOPPED event handling for response +// process. +void CaptureControllerImpl::StopPreview() { + assert(capture_engine_); + + if (!IsInitialized() && !preview_handler_) { + return; + } + + // Requests to stop preview. + preview_handler_->StopPreview(capture_engine_.Get()); +} + +// Marks preview as paused. +// When preview is paused, captured frames are not processed for preview +// and flutter texture is not updated +void CaptureControllerImpl::PausePreview() { + assert(capture_controller_listener_); + + if (!preview_handler_ && !preview_handler_->IsInitialized()) { + return capture_controller_listener_->OnPausePreviewFailed( + "Preview not started"); + } + + if (preview_handler_->PausePreview()) { + capture_controller_listener_->OnPausePreviewSucceeded(); + } else { + capture_controller_listener_->OnPausePreviewFailed( + "Failed to pause preview"); + } +} + +// Marks preview as not paused. +// When preview is not paused, captured frames are processed for preview +// and flutter texture is updated. +void CaptureControllerImpl::ResumePreview() { + assert(capture_controller_listener_); + + if (!preview_handler_ && !preview_handler_->IsInitialized()) { + return capture_controller_listener_->OnResumePreviewFailed( + "Preview not started"); + } + + if (preview_handler_->ResumePreview()) { + capture_controller_listener_->OnResumePreviewSucceeded(); + } else { + capture_controller_listener_->OnResumePreviewFailed( + "Failed to pause preview"); + } +} + +// Handles capture engine events. +// Called via IMFCaptureEngineOnEventCallback implementation. +// Implements CaptureEngineObserver::OnEvent. +void CaptureControllerImpl::OnEvent(IMFMediaEvent* event) { + if (!IsInitialized() && + capture_engine_state_ != CaptureEngineState::kInitializing) { + return; + } + + GUID extended_type_guid; + if (SUCCEEDED(event->GetExtendedType(&extended_type_guid))) { + std::string error; + + HRESULT event_hr; + if (FAILED(event->GetStatus(&event_hr))) { + return; + } + + if (FAILED(event_hr)) { + // Reads system error + _com_error err(event_hr); + error = Utf8FromUtf16(err.ErrorMessage()); + } + + if (extended_type_guid == MF_CAPTURE_ENGINE_ERROR) { + OnCaptureEngineError(event_hr, error); + } else if (extended_type_guid == MF_CAPTURE_ENGINE_INITIALIZED) { + OnCaptureEngineInitialized(SUCCEEDED(event_hr), 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); + } else if (extended_type_guid == MF_CAPTURE_ENGINE_RECORD_STARTED) { + OnRecordStarted(SUCCEEDED(event_hr), error); + } else if (extended_type_guid == MF_CAPTURE_ENGINE_RECORD_STOPPED) { + OnRecordStopped(SUCCEEDED(event_hr), error); + } else if (extended_type_guid == MF_CAPTURE_ENGINE_PHOTO_TAKEN) { + OnPicture(SUCCEEDED(event_hr), error); + } else if (extended_type_guid == MF_CAPTURE_ENGINE_CAMERA_STREAM_BLOCKED) { + // TODO: Inform capture state to flutter. + } else if (extended_type_guid == + MF_CAPTURE_ENGINE_CAMERA_STREAM_UNBLOCKED) { + // TODO: Inform capture state to flutter. + } + } +} + +// Handles Picture event and informs CaptureControllerListener. +void CaptureControllerImpl::OnPicture(bool success, const std::string& error) { + if (success && photo_handler_) { + if (capture_controller_listener_) { + std::string path = photo_handler_->GetPhotoPath(); + capture_controller_listener_->OnTakePictureSucceeded(path); + } + photo_handler_->OnPhotoTaken(); + } else { + if (capture_controller_listener_) { + capture_controller_listener_->OnTakePictureFailed(error); + } + // Destroy photo handler on error cases to make sure state is resetted. + photo_handler_ = nullptr; + } +} + +// Handles CaptureEngineInitialized event and informs +// CaptureControllerListener. +void CaptureControllerImpl::OnCaptureEngineInitialized( + bool success, const std::string& error) { + if (capture_controller_listener_) { + // Create texture handler and register new texture. + texture_handler_ = std::make_unique(texture_registrar_); + + int64_t texture_id = texture_handler_->RegisterTexture(); + if (texture_id >= 0) { + capture_controller_listener_->OnCreateCaptureEngineSucceeded(texture_id); + capture_engine_state_ = CaptureEngineState::kInitialized; + } else { + capture_controller_listener_->OnCreateCaptureEngineFailed( + "Failed to create texture_id"); + // Reset state + ResetCaptureController(); + } + } +} + +// Handles CaptureEngineError event and informs CaptureControllerListener. +void CaptureControllerImpl::OnCaptureEngineError(HRESULT hr, + const std::string& error) { + if (capture_controller_listener_) { + capture_controller_listener_->OnCaptureError(error); + } + + // TODO: If MF_CAPTURE_ENGINE_ERROR is returned, + // should capture controller be reinitialized automatically? +} + +// 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, + const std::string& error) { + if (preview_handler_ && success) { + preview_handler_->OnPreviewStarted(); + } else { + // Destroy preview handler on error cases to make sure state is resetted. + preview_handler_ = nullptr; + } + + if (capture_controller_listener_) { + if (success && preview_frame_width_ > 0 && preview_frame_height_ > 0) { + capture_controller_listener_->OnStartPreviewSucceeded( + preview_frame_width_, preview_frame_height_); + } else { + capture_controller_listener_->OnStartPreviewFailed(error); + } + } +}; + +// Handles PreviewStopped event. +void CaptureControllerImpl::OnPreviewStopped(bool success, + const std::string& error) { + // Preview handler is destroyed if preview is stopped as it + // does not have any use anymore. + preview_handler_ = nullptr; +}; + +// Handles RecordStarted event and informs CaptureControllerListener. +void CaptureControllerImpl::OnRecordStarted(bool success, + const std::string& error) { + if (success && record_handler_) { + record_handler_->OnRecordStarted(); + if (capture_controller_listener_) { + capture_controller_listener_->OnStartRecordSucceeded(); + } + } else { + if (capture_controller_listener_) { + capture_controller_listener_->OnStartRecordFailed(error); + } + + // Destroy record handler on error cases to make sure state is resetted. + record_handler_ = nullptr; + } +}; + +// Handles RecordStopped event and informs CaptureControllerListener. +void CaptureControllerImpl::OnRecordStopped(bool success, + 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) { + std::string path = record_handler_->GetRecordPath(); + capture_controller_listener_->OnStopRecordSucceeded(path); + if (record_handler_->IsTimedRecording()) { + capture_controller_listener_->OnVideoRecordSucceeded( + path, (record_handler_->GetRecordedDuration() / 1000)); + } + } else { + capture_controller_listener_->OnStopRecordFailed(error); + if (record_handler_->IsTimedRecording()) { + capture_controller_listener_->OnVideoRecordFailed(error); + } + } + } + + if (success && record_handler_) { + record_handler_->OnRecordStopped(); + } else { + // Destroy record handler on error cases to make sure state is resetted. + record_handler_ = nullptr; + } +} + +// Updates texture handlers buffer with given data. +// Called via IMFCaptureEngineOnSampleCallback implementation. +// Implements CaptureEngineObserver::UpdateBuffer. +bool CaptureControllerImpl::UpdateBuffer(uint8_t* buffer, + uint32_t data_length) { + if (!texture_handler_) { + return false; + } + return texture_handler_->UpdateBuffer(buffer, data_length); +} + +// Handles capture time update from each processed frame. +// Stops timed recordings if requested recording duration has passed. +// Called via IMFCaptureEngineOnSampleCallback implementation. +// Implements CaptureEngineObserver::UpdateCaptureTime. +void CaptureControllerImpl::UpdateCaptureTime(uint64_t capture_time_us) { + if (!IsInitialized()) { + return; + } + + if (preview_handler_ && preview_handler_->IsStarting()) { + // Informs that first frame is captured succeffully and preview has + // started. + OnPreviewStarted(true, ""); + } + + // Checks if max_video_duration_ms is passed. + if (record_handler_) { + record_handler_->UpdateRecordingTime(capture_time_us); + if (record_handler_->ShouldStopTimedRecording()) { + StopTimedRecord(); + } + } +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/capture_controller.h b/packages/camera/camera_windows/windows/capture_controller.h new file mode 100644 index 000000000000..34e378109d8f --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_controller.h @@ -0,0 +1,292 @@ +// Copyright 2013 The Flutter 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_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "capture_controller_listener.h" +#include "capture_engine_listener.h" +#include "photo_handler.h" +#include "preview_handler.h" +#include "record_handler.h" +#include "texture_handler.h" + +namespace camera_windows { +using flutter::TextureRegistrar; +using Microsoft::WRL::ComPtr; + +// Camera resolution presets. Used to request a capture resolution. +enum class ResolutionPreset { + // Automatic resolution, uses the highest resolution available. + kAuto, + // 240p (320x240) + kLow, + // 480p (720x480) + kMedium, + // 720p (1280x720) + kHigh, + // 1080p (1920x1080) + kVeryHigh, + // 2160p (4096x2160) + kUltraHigh, + // The highest resolution available. + kMax, +}; + +// Camera capture engine state. +// +// On creation, |CaptureControllers| start in state |kNotInitialized|. +// On initialization, the capture controller transitions to the |kInitializing| +// and then |kInitialized| state. +enum class CaptureEngineState { kNotInitialized, kInitializing, kInitialized }; + +// Interface for a class that enumerates video capture device sources. +class VideoCaptureDeviceEnumerator { + private: + virtual bool EnumerateVideoCaptureDeviceSources(IMFActivate*** devices, + UINT32* count) = 0; +}; + +// Interface implemented by capture controllers. +// +// Capture controllers are used to capture video streams or still photos from +// their associated |Camera|. +class CaptureController { + public: + CaptureController() {} + virtual ~CaptureController() = default; + + // Disallow copy and move. + CaptureController(const CaptureController&) = delete; + CaptureController& operator=(const CaptureController&) = delete; + + // Initializes the capture controller with the specified device id. + // + // 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 + // be captured. + // 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, + const std::string& device_id, + bool record_audio, + ResolutionPreset resolution_preset) = 0; + + // Returns preview frame width + virtual uint32_t GetPreviewWidth() const = 0; + + // Returns preview frame height + virtual uint32_t GetPreviewHeight() const = 0; + + // Starts the preview. + virtual void StartPreview() = 0; + + // Pauses the preview. + virtual void PausePreview() = 0; + + // Resumes the preview. + virtual void ResumePreview() = 0; + + // Starts recording video. + virtual void StartRecord(const std::string& file_path, + int64_t max_video_duration_ms) = 0; + + // Stops the current video recording. + virtual void StopRecord() = 0; + + // Captures a still photo. + virtual void TakePicture(const std::string& file_path) = 0; +}; + +// Concrete implementation of the |CaptureController| interface. +// +// Handles the video preview stream via a |PreviewHandler| instance, video +// capture via a |RecordHandler| instance, and still photo capture via a +// |PhotoHandler| instance. +class CaptureControllerImpl : public CaptureController, + public CaptureEngineObserver { + public: + static bool EnumerateVideoCaptureDeviceSources(IMFActivate*** devices, + UINT32* count); + + explicit CaptureControllerImpl(CaptureControllerListener* listener); + virtual ~CaptureControllerImpl(); + + // Disallow copy and move. + CaptureControllerImpl(const CaptureControllerImpl&) = delete; + CaptureControllerImpl& operator=(const CaptureControllerImpl&) = delete; + + // CaptureController + void 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_; } + uint32_t GetPreviewHeight() const override { return preview_frame_height_; } + void StartPreview() override; + void PausePreview() override; + void ResumePreview() override; + void StartRecord(const std::string& file_path, + int64_t max_video_duration_ms) override; + void StopRecord() override; + void TakePicture(const std::string& file_path) override; + + // CaptureEngineObserver + void OnEvent(IMFMediaEvent* event) override; + bool IsReadyForSample() const override { + return capture_engine_state_ == CaptureEngineState::kInitialized && + preview_handler_ && preview_handler_->IsRunning(); + } + bool UpdateBuffer(uint8_t* data, uint32_t data_length) override; + void UpdateCaptureTime(uint64_t capture_time) override; + + // Sets capture engine, for testing purposes. + void SetCaptureEngine(IMFCaptureEngine* capture_engine) { + capture_engine_ = capture_engine; + } + + // Sets video source, for testing purposes. + void SetVideoSource(IMFMediaSource* video_source) { + video_source_ = video_source; + } + + // Sets audio source, for testing purposes. + void SetAudioSource(IMFMediaSource* audio_source) { + audio_source_ = audio_source; + } + + private: + // Helper function to return initialized state as boolean; + bool IsInitialized() const { + return capture_engine_state_ == CaptureEngineState::kInitialized; + } + + // Resets capture controller state. + // This is called if capture engine creation fails or is disposed. + void ResetCaptureController(); + + // Returns max preview height calculated from resolution present. + uint32_t GetMaxPreviewHeight() const; + + // Uses first audio source to capture audio. + // Note: Enumerating audio sources via platform interface is not supported. + HRESULT CreateDefaultAudioCaptureSource(); + + // Initializes video capture source from camera device. + HRESULT CreateVideoCaptureSourceForDevice(const std::string& video_device_id); + + // Creates DX11 Device and D3D Manager. + HRESULT CreateD3DManagerWithDX11Device(); + + // Initializes capture engine object. + HRESULT CreateCaptureEngine(); + + // Enumerates video_sources media types and finds out best resolution + // for preview and video capture. + HRESULT FindBaseMediaTypes(); + + // Stops timed video record. Called internally when record handler when max + // recording time is exceeded. + void StopTimedRecord(); + + // Stops preview. Called internally on camera reset and dispose. + void StopPreview(); + + // Handles capture engine initalization event. + void OnCaptureEngineInitialized(bool success, const std::string& error); + + // Handles capture engine errors. + void OnCaptureEngineError(HRESULT hr, const std::string& error); + + // Handles picture events. + void OnPicture(bool success, const std::string& error); + + // Handles preview started events. + void OnPreviewStarted(bool success, const std::string& error); + + // Handles preview stopped events. + void OnPreviewStopped(bool success, const std::string& error); + + // Handles record started events. + void OnRecordStarted(bool success, const std::string& error); + + // Handles record stopped events. + void OnRecordStopped(bool success, const std::string& error); + + bool media_foundation_started_ = false; + bool record_audio_ = false; + uint32_t preview_frame_width_ = 0; + uint32_t preview_frame_height_ = 0; + UINT dx_device_reset_token_ = 0; + std::unique_ptr record_handler_; + std::unique_ptr preview_handler_; + std::unique_ptr photo_handler_; + std::unique_ptr texture_handler_; + CaptureControllerListener* capture_controller_listener_; + + std::string video_device_id_; + CaptureEngineState capture_engine_state_ = + CaptureEngineState::kNotInitialized; + ResolutionPreset resolution_preset_ = ResolutionPreset::kMedium; + ComPtr capture_engine_; + ComPtr capture_engine_callback_handler_; + ComPtr dxgi_device_manager_; + ComPtr dx11_device_; + ComPtr base_capture_media_type_; + ComPtr base_preview_media_type_; + ComPtr video_source_; + ComPtr audio_source_; + + TextureRegistrar* texture_registrar_ = nullptr; +}; + +// Inferface for factory classes that create |CaptureController| instances. +class CaptureControllerFactory { + public: + CaptureControllerFactory() {} + virtual ~CaptureControllerFactory() = default; + + // Disallow copy and move. + CaptureControllerFactory(const CaptureControllerFactory&) = delete; + CaptureControllerFactory& operator=(const CaptureControllerFactory&) = delete; + + // Create and return a |CaptureController| that makes callbacks on the + // specified |CaptureControllerListener|, which must not be null. + virtual std::unique_ptr CreateCaptureController( + CaptureControllerListener* listener) = 0; +}; + +// Concreate implementation of |CaptureControllerFactory|. +class CaptureControllerFactoryImpl : public CaptureControllerFactory { + public: + CaptureControllerFactoryImpl() {} + virtual ~CaptureControllerFactoryImpl() = default; + + // Disallow copy and move. + CaptureControllerFactoryImpl(const CaptureControllerFactoryImpl&) = delete; + CaptureControllerFactoryImpl& operator=(const CaptureControllerFactoryImpl&) = + delete; + + std::unique_ptr CreateCaptureController( + CaptureControllerListener* listener) override { + return std::make_unique(listener); + } +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_H_ diff --git a/packages/camera/camera_windows/windows/capture_controller_listener.h b/packages/camera/camera_windows/windows/capture_controller_listener.h new file mode 100644 index 000000000000..0e713ea7af18 --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_controller_listener.h @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_LISTENER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_LISTENER_H_ + +#include + +namespace camera_windows { + +// Interface for classes that receives callbacks on events from the associated +// |CaptureController|. +class CaptureControllerListener { + public: + virtual ~CaptureControllerListener() = default; + + // Called by CaptureController on successful capture engine initialization. + // + // texture_id: A 64bit integer id registered by TextureRegistrar + virtual void OnCreateCaptureEngineSucceeded(int64_t texture_id) = 0; + + // Called by CaptureController if initializing the capture engine fails. + // + // error: A string describing the error. + virtual void OnCreateCaptureEngineFailed(const std::string& error) = 0; + + // Called by CaptureController on successfully started preview. + // + // width: Preview frame width. + // height: Preview frame height. + virtual void OnStartPreviewSucceeded(int32_t width, int32_t height) = 0; + + // Called by CaptureController if starting the preview fails. + // + // error: A string describing the error. + virtual void OnStartPreviewFailed(const std::string& error) = 0; + + // Called by CaptureController on successfully paused preview. + virtual void OnPausePreviewSucceeded() = 0; + + // Called by CaptureController if pausing the preview fails. + // + // error: A string describing the error. + virtual void OnPausePreviewFailed(const std::string& error) = 0; + + // Called by CaptureController on successfully resumed preview. + virtual void OnResumePreviewSucceeded() = 0; + + // Called by CaptureController if resuming the preview fails. + // + // error: A string describing the error. + virtual void OnResumePreviewFailed(const std::string& error) = 0; + + // Called by CaptureController on successfully started recording. + virtual void OnStartRecordSucceeded() = 0; + + // Called by CaptureController if starting the recording fails. + // + // error: A string describing the error. + virtual void OnStartRecordFailed(const std::string& error) = 0; + + // Called by CaptureController on successfully stopped recording. + // + // file_path: Filesystem path of the recorded video file. + virtual void OnStopRecordSucceeded(const std::string& file_path) = 0; + + // Called by CaptureController if stopping the recording fails. + // + // error: A string describing the error. + virtual void OnStopRecordFailed(const std::string& error) = 0; + + // Called by CaptureController on successfully captured picture. + // + // file_path: Filesystem path of the captured image. + virtual void OnTakePictureSucceeded(const std::string& file_path) = 0; + + // Called by CaptureController if taking picture fails. + // + // error: A string describing the error. + virtual void OnTakePictureFailed(const std::string& error) = 0; + + // Called by CaptureController when timed recording is successfully recorded. + // + // file_path: Filesystem path of the captured image. + // video_duration: Duration of recorded video in milliseconds. + virtual void OnVideoRecordSucceeded(const std::string& file_path, + int64_t video_duration_ms) = 0; + + // Called by CaptureController if timed recording fails. + // + // error: A string describing the error. + virtual void OnVideoRecordFailed(const std::string& error) = 0; + + // Called by CaptureController if capture engine returns error. + // For example when camera is disconnected while on use. + // + // error: A string describing the error. + virtual void OnCaptureError(const std::string& error) = 0; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_CONTROLLER_LISTENER_H_ diff --git a/packages/camera/camera_windows/windows/capture_device_info.cpp b/packages/camera/camera_windows/windows/capture_device_info.cpp new file mode 100644 index 000000000000..446056a71c44 --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_device_info.cpp @@ -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. + +#include "capture_device_info.h" + +#include +#include + +namespace camera_windows { +std::string CaptureDeviceInfo::GetUniqueDeviceName() const { + return display_name_ + " <" + device_id_ + ">"; +} + +bool CaptureDeviceInfo::ParseDeviceInfoFromCameraName( + const std::string& camera_name) { + size_t delimeter_index = camera_name.rfind(' ', camera_name.length()); + if (delimeter_index != std::string::npos) { + auto deviceInfo = std::make_unique(); + display_name_ = camera_name.substr(0, delimeter_index); + device_id_ = camera_name.substr(delimeter_index + 2, + camera_name.length() - delimeter_index - 3); + return true; + } + + return false; +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/capture_device_info.h b/packages/camera/camera_windows/windows/capture_device_info.h new file mode 100644 index 000000000000..63ffa8571092 --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_device_info.h @@ -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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_DEVICE_INFO_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_DEVICE_INFO_H_ + +#include + +namespace camera_windows { + +// Name and device ID information for a capture device. +class CaptureDeviceInfo { + public: + CaptureDeviceInfo() {} + virtual ~CaptureDeviceInfo() = default; + + // Disallow copy and move. + CaptureDeviceInfo(const CaptureDeviceInfo&) = delete; + CaptureDeviceInfo& operator=(const CaptureDeviceInfo&) = delete; + + // Build unique device name from display name and device id. + // Format: "display_name ". + std::string GetUniqueDeviceName() const; + + // Parses display name and device id from unique device name format. + // Format: "display_name ". + bool CaptureDeviceInfo::ParseDeviceInfoFromCameraName( + const std::string& camera_name); + + // Updates display name. + void SetDisplayName(const std::string& display_name) { + display_name_ = display_name; + } + + // Updates device id. + void SetDeviceID(const std::string& device_id) { device_id_ = device_id; } + + // Returns device id. + std::string GetDeviceId() const { return device_id_; } + + private: + std::string display_name_; + std::string device_id_; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_DEVICE_INFO_H_ diff --git a/packages/camera/camera_windows/windows/capture_engine_listener.cpp b/packages/camera/camera_windows/windows/capture_engine_listener.cpp new file mode 100644 index 000000000000..5425b388287a --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_engine_listener.cpp @@ -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. + +#include "capture_engine_listener.h" + +#include +#include + +namespace camera_windows { + +using Microsoft::WRL::ComPtr; + +// IUnknown +STDMETHODIMP_(ULONG) CaptureEngineListener::AddRef() { + return InterlockedIncrement(&ref_); +} + +// IUnknown +STDMETHODIMP_(ULONG) +CaptureEngineListener::Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; +} + +// IUnknown +STDMETHODIMP_(HRESULT) +CaptureEngineListener::QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFCaptureEngineOnEventCallback) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } else if (riid == IID_IMFCaptureEngineOnSampleCallback) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP CaptureEngineListener::OnEvent(IMFMediaEvent* event) { + if (observer_) { + observer_->OnEvent(event); + } + return S_OK; +} + +// IMFCaptureEngineOnSampleCallback +HRESULT CaptureEngineListener::OnSample(IMFSample* sample) { + HRESULT hr = S_OK; + + if (this->observer_ && sample) { + LONGLONG raw_time_stamp = 0; + // Receives the presentation time, in 100-nanosecond units. + sample->GetSampleTime(&raw_time_stamp); + + // Report time in microseconds. + this->observer_->UpdateCaptureTime( + static_cast(raw_time_stamp / 10)); + + if (!this->observer_->IsReadyForSample()) { + // No texture target available or not previewing, just return status. + return hr; + } + + ComPtr buffer; + hr = sample->ConvertToContiguousBuffer(&buffer); + + // Draw the frame. + if (SUCCEEDED(hr) && buffer) { + DWORD max_length = 0; + DWORD current_length = 0; + uint8_t* data; + if (SUCCEEDED(buffer->Lock(&data, &max_length, ¤t_length))) { + this->observer_->UpdateBuffer(data, current_length); + } + hr = buffer->Unlock(); + } + } + return hr; +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/capture_engine_listener.h b/packages/camera/camera_windows/windows/capture_engine_listener.h new file mode 100644 index 000000000000..081e3ea0f764 --- /dev/null +++ b/packages/camera/camera_windows/windows/capture_engine_listener.h @@ -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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_ENGINE_LISTENER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_ENGINE_LISTENER_H_ + +#include + +#include +#include + +namespace camera_windows { + +// A class that implements callbacks for events from a |CaptureEngineListener|. +class CaptureEngineObserver { + public: + virtual ~CaptureEngineObserver() = default; + + // Returns true if sample can be processed. + virtual bool IsReadyForSample() const = 0; + + // Handles Capture Engine media events. + virtual void OnEvent(IMFMediaEvent* event) = 0; + + // Updates texture buffer + virtual bool UpdateBuffer(uint8_t* data, uint32_t new_length) = 0; + + // Handles capture timestamps updates. + // Used to stop timed recordings when recorded time is exceeded. + virtual void UpdateCaptureTime(uint64_t capture_time) = 0; +}; + +// Listener for Windows Media Foundation capture engine events and samples. +// +// Events are redirected to observers for processing. Samples are preprosessed +// and sent to the associated observer if it is ready to process samples. +class CaptureEngineListener : public IMFCaptureEngineOnSampleCallback, + public IMFCaptureEngineOnEventCallback { + public: + CaptureEngineListener(CaptureEngineObserver* observer) : observer_(observer) { + assert(observer); + } + + ~CaptureEngineListener() {} + + // Disallow copy and move. + CaptureEngineListener(const CaptureEngineListener&) = delete; + CaptureEngineListener& operator=(const CaptureEngineListener&) = delete; + + // IUnknown + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv); + + // IMFCaptureEngineOnEventCallback + STDMETHODIMP OnEvent(IMFMediaEvent* pEvent); + + // IMFCaptureEngineOnSampleCallback + STDMETHODIMP_(HRESULT) OnSample(IMFSample* pSample); + + private: + CaptureEngineObserver* observer_; + volatile ULONG ref_ = 0; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_CAPTURE_ENGINE_LISTENER_H_ diff --git a/packages/camera/camera_windows/windows/com_heap_ptr.h b/packages/camera/camera_windows/windows/com_heap_ptr.h new file mode 100644 index 000000000000..a314ed3c8878 --- /dev/null +++ b/packages/camera/camera_windows/windows/com_heap_ptr.h @@ -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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_COMHEAPPTR_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_COMHEAPPTR_H_ + +#include + +#include + +namespace camera_windows { +// Wrapper for COM object for automatic memory release support +// Destructor uses CoTaskMemFree to release memory allocations. +template +class ComHeapPtr { + public: + ComHeapPtr() : p_obj_(nullptr) {} + ComHeapPtr(T* p_obj) : p_obj_(p_obj) {} + + // Frees memory on destruction. + ~ComHeapPtr() { Free(); } + + // Prevent copying / ownership transfer as not currently needed. + ComHeapPtr(ComHeapPtr const&) = delete; + ComHeapPtr& operator=(ComHeapPtr const&) = delete; + + // Returns the pointer to the memory. + operator T*() { return p_obj_; } + + // Returns the pointer to the memory. + T* operator->() { + assert(p_obj_ != nullptr); + return p_obj_; + } + + // Returns the pointer to the memory. + const T* operator->() const { + assert(p_obj_ != nullptr); + return p_obj_; + } + + // Returns the pointer to the memory. + T** operator&() { + // Wrapped object must be nullptr to avoid memory leaks. + // Object can be released with Reset(nullptr). + assert(p_obj_ == nullptr); + return &p_obj_; + } + + // Frees the memory pointed to, and sets the pointer to nullptr. + void Free() { + if (p_obj_) { + CoTaskMemFree(p_obj_); + } + p_obj_ = nullptr; + } + + private: + // Pointer to memory. + T* p_obj_; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_COMHEAPPTR_H_ diff --git a/packages/camera/camera_windows/windows/include/camera_windows/camera_windows.h b/packages/camera/camera_windows/windows/include/camera_windows/camera_windows.h new file mode 100644 index 000000000000..b1e28b8aa8df --- /dev/null +++ b/packages/camera/camera_windows/windows/include/camera_windows/camera_windows.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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_INCLUDE_CAMERA_WINDOWS_CAMERA_WINDOWS_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_INCLUDE_CAMERA_WINDOWS_CAMERA_WINDOWS_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 CameraWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_INCLUDE_CAMERA_WINDOWS_CAMERA_WINDOWS_H_ diff --git a/packages/camera/camera_windows/windows/photo_handler.cpp b/packages/camera/camera_windows/windows/photo_handler.cpp new file mode 100644 index 000000000000..10df230c2cf2 --- /dev/null +++ b/packages/camera/camera_windows/windows/photo_handler.cpp @@ -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. + +#include "photo_handler.h" + +#include +#include +#include + +#include + +#include "capture_engine_listener.h" +#include "string_utils.h" + +namespace camera_windows { + +using Microsoft::WRL::ComPtr; + +// Initializes media type for photo capture for jpeg images. +HRESULT BuildMediaTypeForPhotoCapture(IMFMediaType* src_media_type, + IMFMediaType** photo_media_type, + GUID image_format) { + assert(src_media_type); + ComPtr new_media_type; + + HRESULT hr = MFCreateMediaType(&new_media_type); + if (FAILED(hr)) { + return hr; + } + + // Clones everything from original media type. + hr = src_media_type->CopyAllItems(new_media_type.Get()); + if (FAILED(hr)) { + return hr; + } + + hr = new_media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Image); + if (FAILED(hr)) { + return hr; + } + + hr = new_media_type->SetGUID(MF_MT_SUBTYPE, image_format); + if (FAILED(hr)) { + return hr; + } + + new_media_type.CopyTo(photo_media_type); + return hr; +} + +HRESULT PhotoHandler::InitPhotoSink(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type) { + assert(capture_engine); + assert(base_media_type); + + HRESULT hr = S_OK; + + if (photo_sink_) { + // If photo sink already exists, only update output filename. + hr = photo_sink_->SetOutputFileName(Utf16FromUtf8(file_path_).c_str()); + + if (FAILED(hr)) { + photo_sink_ = nullptr; + } + + return hr; + } + + ComPtr photo_media_type; + ComPtr capture_sink; + + // Get sink with photo type. + hr = + capture_engine->GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PHOTO, &capture_sink); + if (FAILED(hr)) { + return hr; + } + + hr = capture_sink.As(&photo_sink_); + if (FAILED(hr)) { + photo_sink_ = nullptr; + return hr; + } + + hr = photo_sink_->RemoveAllStreams(); + if (FAILED(hr)) { + photo_sink_ = nullptr; + return hr; + } + + hr = BuildMediaTypeForPhotoCapture(base_media_type, + photo_media_type.GetAddressOf(), + GUID_ContainerFormatJpeg); + + if (FAILED(hr)) { + photo_sink_ = nullptr; + return hr; + } + + DWORD photo_sink_stream_index; + hr = photo_sink_->AddStream( + (DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_PHOTO, + photo_media_type.Get(), nullptr, &photo_sink_stream_index); + if (FAILED(hr)) { + photo_sink_ = nullptr; + return hr; + } + + hr = photo_sink_->SetOutputFileName(Utf16FromUtf8(file_path_).c_str()); + if (FAILED(hr)) { + photo_sink_ = nullptr; + return hr; + } + + return hr; +} + +bool 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; + } + + photo_state_ = PhotoState::kTakingPhoto; + return SUCCEEDED(capture_engine->TakePhoto()); +} + +void PhotoHandler::OnPhotoTaken() { + assert(photo_state_ == PhotoState::kTakingPhoto); + photo_state_ = PhotoState::kIdle; +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/photo_handler.h b/packages/camera/camera_windows/windows/photo_handler.h new file mode 100644 index 000000000000..ef0d98bfc45f --- /dev/null +++ b/packages/camera/camera_windows/windows/photo_handler.h @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter 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_CAMERA_CAMERA_WINDOWS_WINDOWS_PHOTO_HANDLER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_PHOTO_HANDLER_H_ + +#include +#include +#include + +#include +#include + +#include "capture_engine_listener.h" + +namespace camera_windows { +using Microsoft::WRL::ComPtr; + +// Various states that the photo handler can be in. +// +// When created, the handler is in |kNotStarted| state and transtions in +// sequential order through the states. +enum class PhotoState { + kNotStarted, + kIdle, + kTakingPhoto, +}; + +// Handles photo sink initialization and tracks photo capture states. +class PhotoHandler { + public: + PhotoHandler() {} + virtual ~PhotoHandler() = default; + + // Prevent copying. + PhotoHandler(PhotoHandler const&) = delete; + PhotoHandler& operator=(PhotoHandler const&) = delete; + + // Initializes photo sink if not initialized and requests the capture engine + // 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); + + // Set the photo handler recording state to: kIdel. + void OnPhotoTaken(); + + // Returns true if photo state is kIdle. + bool IsInitialized() const { return photo_state_ == PhotoState::kIdle; } + + // Returns true if photo state is kTakingPhoto. + bool IsTakingPhoto() const { + return photo_state_ == PhotoState::kTakingPhoto; + } + + // Returns the filesystem path of the captured photo. + std::string GetPhotoPath() const { return file_path_; } + + private: + // Initializes record sink for video file capture. + HRESULT InitPhotoSink(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type); + + std::string file_path_; + PhotoState photo_state_ = PhotoState::kNotStarted; + ComPtr photo_sink_; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_PHOTO_HANDLER_H_ diff --git a/packages/camera/camera_windows/windows/preview_handler.cpp b/packages/camera/camera_windows/windows/preview_handler.cpp new file mode 100644 index 000000000000..d7fb2721259c --- /dev/null +++ b/packages/camera/camera_windows/windows/preview_handler.cpp @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter 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 "preview_handler.h" + +#include +#include + +#include + +#include "capture_engine_listener.h" +#include "string_utils.h" + +namespace camera_windows { + +using Microsoft::WRL::ComPtr; + +// Initializes media type for video preview. +HRESULT BuildMediaTypeForVideoPreview(IMFMediaType* src_media_type, + IMFMediaType** preview_media_type) { + assert(src_media_type); + ComPtr new_media_type; + + HRESULT hr = MFCreateMediaType(&new_media_type); + if (FAILED(hr)) { + return hr; + } + + // Clones everything from original media type. + hr = src_media_type->CopyAllItems(new_media_type.Get()); + if (FAILED(hr)) { + return hr; + } + + // Changes subtype to MFVideoFormat_RGB32. + hr = new_media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32); + if (FAILED(hr)) { + return hr; + } + + hr = new_media_type->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE); + if (FAILED(hr)) { + return hr; + } + + new_media_type.CopyTo(preview_media_type); + + return hr; +} + +HRESULT PreviewHandler::InitPreviewSink( + IMFCaptureEngine* capture_engine, IMFMediaType* base_media_type, + CaptureEngineListener* sample_callback) { + assert(capture_engine); + assert(base_media_type); + assert(sample_callback); + + HRESULT hr = S_OK; + + if (preview_sink_) { + // Preview sink already initialized. + return hr; + } + + ComPtr preview_media_type; + ComPtr capture_sink; + + // Get sink with preview type. + hr = capture_engine->GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_PREVIEW, + &capture_sink); + if (FAILED(hr)) { + return hr; + } + + hr = capture_sink.As(&preview_sink_); + if (FAILED(hr)) { + preview_sink_ = nullptr; + return hr; + } + + hr = preview_sink_->RemoveAllStreams(); + if (FAILED(hr)) { + preview_sink_ = nullptr; + return hr; + } + + hr = BuildMediaTypeForVideoPreview(base_media_type, + preview_media_type.GetAddressOf()); + + if (FAILED(hr)) { + preview_sink_ = nullptr; + return hr; + } + + DWORD preview_sink_stream_index; + hr = preview_sink_->AddStream( + (DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_PREVIEW, + preview_media_type.Get(), nullptr, &preview_sink_stream_index); + + if (FAILED(hr)) { + return hr; + } + + hr = preview_sink_->SetSampleCallback(preview_sink_stream_index, + sample_callback); + + if (FAILED(hr)) { + preview_sink_ = nullptr; + return hr; + } + + return hr; +} + +bool 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; + } + + preview_state_ = PreviewState::kStarting; + return SUCCEEDED(capture_engine->StartPreview()); +} + +bool 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 false; +} + +bool PreviewHandler::PausePreview() { + if (preview_state_ != PreviewState::kRunning) { + return false; + } + preview_state_ = PreviewState::kPaused; + return true; +} + +bool PreviewHandler::ResumePreview() { + if (preview_state_ != PreviewState::kPaused) { + return false; + } + preview_state_ = PreviewState::kRunning; + return true; +} + +void PreviewHandler::OnPreviewStarted() { + assert(preview_state_ == PreviewState::kStarting); + if (preview_state_ == PreviewState::kStarting) { + preview_state_ = PreviewState::kRunning; + } +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/preview_handler.h b/packages/camera/camera_windows/windows/preview_handler.h new file mode 100644 index 000000000000..97b85fc28568 --- /dev/null +++ b/packages/camera/camera_windows/windows/preview_handler.h @@ -0,0 +1,103 @@ +// Copyright 2013 The Flutter 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_CAMERA_CAMERA_WINDOWS_WINDOWS_PREVIEW_HANDLER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_PREVIEW_HANDLER_H_ + +#include +#include +#include + +#include +#include + +#include "capture_engine_listener.h" + +namespace camera_windows { +using Microsoft::WRL::ComPtr; + +// States the preview handler can be in. +// +// When created, the handler starts in |kNotStarted| state and mostly +// transitions in sequential order of the states. When the preview is running, +// it can be set to the |kPaused| state and later resumed to |kRunning| state. +enum class PreviewState { + kNotStarted, + kStarting, + kRunning, + kPaused, + kStopping +}; + +// Handler for a camera's video preview. +// +// Handles preview sink initialization and manages the state of the video +// preview. +class PreviewHandler { + public: + PreviewHandler() {} + virtual ~PreviewHandler() = default; + + // Prevent copying. + PreviewHandler(PreviewHandler const&) = delete; + PreviewHandler& operator=(PreviewHandler const&) = delete; + + // 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. + // base_media_type: A pointer to base media type used as a base + // 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); + + // 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); + + // Set the preview handler recording state to: paused. + bool PausePreview(); + + // Set the preview handler recording state to: running. + bool ResumePreview(); + + // Set the preview handler recording state to: running. + void OnPreviewStarted(); + + // Returns true if preview state is running or paused. + bool IsInitialized() const { + return preview_state_ == PreviewState::kRunning && + preview_state_ == PreviewState::kPaused; + } + + // Returns true if preview state is running. + bool IsRunning() const { return preview_state_ == PreviewState::kRunning; } + + // Return true if preview state is paused. + bool IsPaused() const { return preview_state_ == PreviewState::kPaused; } + + // Returns true if preview state is starting. + bool IsStarting() const { return preview_state_ == PreviewState::kStarting; } + + private: + // Initializes record sink for video file capture. + HRESULT InitPreviewSink(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type, + CaptureEngineListener* sample_callback); + + PreviewState preview_state_ = PreviewState::kNotStarted; + ComPtr preview_sink_; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_PREVIEW_HANDLER_H_ diff --git a/packages/camera/camera_windows/windows/record_handler.cpp b/packages/camera/camera_windows/windows/record_handler.cpp new file mode 100644 index 000000000000..1cb258e162a5 --- /dev/null +++ b/packages/camera/camera_windows/windows/record_handler.cpp @@ -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. + +#include "record_handler.h" + +#include +#include + +#include + +#include "string_utils.h" + +namespace camera_windows { + +using Microsoft::WRL::ComPtr; + +// Initializes media type for video capture. +HRESULT BuildMediaTypeForVideoCapture(IMFMediaType* src_media_type, + IMFMediaType** video_record_media_type, + GUID capture_format) { + assert(src_media_type); + ComPtr new_media_type; + + HRESULT hr = MFCreateMediaType(&new_media_type); + if (FAILED(hr)) { + return hr; + } + + // Clones everything from original media type. + hr = src_media_type->CopyAllItems(new_media_type.Get()); + if (FAILED(hr)) { + return hr; + } + + hr = new_media_type->SetGUID(MF_MT_SUBTYPE, capture_format); + if (FAILED(hr)) { + return hr; + } + + new_media_type.CopyTo(video_record_media_type); + return S_OK; +} + +// Queries interface object from collection. +template +HRESULT GetCollectionObject(IMFCollection* pCollection, DWORD index, + Q** ppObj) { + ComPtr pUnk; + HRESULT hr = pCollection->GetElement(index, pUnk.GetAddressOf()); + if (FAILED(hr)) { + return hr; + } + return pUnk->QueryInterface(IID_PPV_ARGS(ppObj)); +} + +// Initializes media type for audo capture. +HRESULT BuildMediaTypeForAudioCapture(IMFMediaType** audio_record_media_type) { + ComPtr audio_output_attributes; + ComPtr src_media_type; + ComPtr new_media_type; + ComPtr available_output_types; + DWORD mt_count = 0; + + HRESULT hr = MFCreateAttributes(&audio_output_attributes, 1); + if (FAILED(hr)) { + return hr; + } + + // Enumerates only low latency audio outputs. + hr = audio_output_attributes->SetUINT32(MF_LOW_LATENCY, TRUE); + if (FAILED(hr)) { + return hr; + } + + DWORD mft_flags = (MFT_ENUM_FLAG_ALL & (~MFT_ENUM_FLAG_FIELDOFUSE)) | + MFT_ENUM_FLAG_SORTANDFILTER; + + hr = MFTranscodeGetAudioOutputAvailableTypes( + MFAudioFormat_AAC, mft_flags, audio_output_attributes.Get(), + available_output_types.GetAddressOf()); + if (FAILED(hr)) { + return hr; + } + + hr = GetCollectionObject(available_output_types.Get(), 0, + src_media_type.GetAddressOf()); + if (FAILED(hr)) { + return hr; + } + + hr = available_output_types->GetElementCount(&mt_count); + if (FAILED(hr)) { + return hr; + } + + if (mt_count == 0) { + // No sources found, mark process as failure. + return E_FAIL; + } + + // Create new media type to copy original media type to. + hr = MFCreateMediaType(&new_media_type); + if (FAILED(hr)) { + return hr; + } + + hr = src_media_type->CopyAllItems(new_media_type.Get()); + if (FAILED(hr)) { + return hr; + } + + new_media_type.CopyTo(audio_record_media_type); + return hr; +} + +HRESULT RecordHandler::InitRecordSink(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type) { + assert(!file_path_.empty()); + assert(capture_engine); + assert(base_media_type); + + HRESULT hr = S_OK; + if (record_sink_) { + // If record sink already exists, only update output filename. + hr = record_sink_->SetOutputFileName(Utf16FromUtf8(file_path_).c_str()); + + if (FAILED(hr)) { + record_sink_ = nullptr; + } + return hr; + } + + ComPtr video_record_media_type; + ComPtr capture_sink; + + // Gets sink from capture engine with record type. + + hr = capture_engine->GetSink(MF_CAPTURE_ENGINE_SINK_TYPE_RECORD, + &capture_sink); + if (FAILED(hr)) { + return hr; + } + + hr = capture_sink.As(&record_sink_); + if (FAILED(hr)) { + return hr; + } + + // Removes existing streams if available. + hr = record_sink_->RemoveAllStreams(); + if (FAILED(hr)) { + return hr; + } + + hr = BuildMediaTypeForVideoCapture(base_media_type, + video_record_media_type.GetAddressOf(), + MFVideoFormat_H264); + if (FAILED(hr)) { + return hr; + } + + DWORD video_record_sink_stream_index; + hr = record_sink_->AddStream( + (DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_RECORD, + video_record_media_type.Get(), nullptr, &video_record_sink_stream_index); + if (FAILED(hr)) { + return hr; + } + + if (record_audio_) { + ComPtr audio_record_media_type; + HRESULT audio_capture_hr = S_OK; + audio_capture_hr = + BuildMediaTypeForAudioCapture(audio_record_media_type.GetAddressOf()); + + if (SUCCEEDED(audio_capture_hr)) { + DWORD audio_record_sink_stream_index; + hr = record_sink_->AddStream( + (DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_AUDIO, + audio_record_media_type.Get(), nullptr, + &audio_record_sink_stream_index); + } + + if (FAILED(hr)) { + return hr; + } + } + + hr = record_sink_->SetOutputFileName(Utf16FromUtf8(file_path_).c_str()); + + return hr; +} + +bool 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); + + type_ = max_duration < 0 ? RecordingType::kContinuous : RecordingType::kTimed; + max_video_duration_ms_ = max_duration; + file_path_ = file_path; + recording_start_timestamp_us_ = -1; + recording_duration_us_ = 0; + + if (FAILED(InitRecordSink(capture_engine, base_media_type))) { + return false; + } + + recording_state_ = RecordState::kStarting; + capture_engine->StartRecord(); + + return true; +} + +bool 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 false; +} + +void RecordHandler::OnRecordStarted() { + if (recording_state_ == RecordState::kStarting) { + recording_state_ = RecordState::kRunning; + } +} + +void RecordHandler::OnRecordStopped() { + if (recording_state_ == RecordState::kStopping) { + file_path_ = ""; + recording_start_timestamp_us_ = -1; + recording_duration_us_ = 0; + max_video_duration_ms_ = -1; + recording_state_ = RecordState::kNotStarted; + } +} + +void RecordHandler::UpdateRecordingTime(uint64_t timestamp) { + if (recording_start_timestamp_us_ < 0) { + recording_start_timestamp_us_ = timestamp; + } + + recording_duration_us_ = (timestamp - recording_start_timestamp_us_); +} + +bool RecordHandler::ShouldStopTimedRecording() const { + return type_ == RecordingType::kTimed && + recording_state_ == RecordState::kRunning && + max_video_duration_ms_ > 0 && + recording_duration_us_ >= + (static_cast(max_video_duration_ms_) * 1000); +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/record_handler.h b/packages/camera/camera_windows/windows/record_handler.h new file mode 100644 index 000000000000..0daa7f6546a1 --- /dev/null +++ b/packages/camera/camera_windows/windows/record_handler.h @@ -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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_RECORD_HANDLER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_RECORD_HANDLER_H_ + +#include +#include +#include + +#include +#include + +namespace camera_windows { +using Microsoft::WRL::ComPtr; + +enum class RecordingType { + // Recording continues until it is stopped with a separate stop command. + kContinuous, + // Recording stops automatically after requested record time is passed. + kTimed +}; + +// States that the record handler can be in. +// +// When created, the handler starts in |kNotStarted| state and transtions in +// sequential order through the states. +enum class RecordState { kNotStarted, kStarting, kRunning, kStopping }; + +// Handler for video recording via the camera. +// +// Handles record sink initialization and manages the state of video recording. +class RecordHandler { + public: + RecordHandler(bool record_audio) : record_audio_(record_audio) {} + virtual ~RecordHandler() = default; + + // Prevent copying. + RecordHandler(RecordHandler const&) = delete; + RecordHandler& operator=(RecordHandler const&) = delete; + + // 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. + // If value is -1 video recording is considered as + // a continuous recording. + // capture_engine: A pointer to capture engine instance. Used to start + // 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); + + // 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); + + // Set the record handler recording state to: running. + void OnRecordStarted(); + + // Resets the record handler state and + // sets recording state to: not started. + void OnRecordStopped(); + + // Returns true if recording type is continuous recording. + bool IsContinuousRecording() const { + return type_ == RecordingType::kContinuous; + } + + // Returns true if recording type is timed recording. + bool IsTimedRecording() const { return type_ == RecordingType::kTimed; } + + // Returns true if new recording can be started. + bool CanStart() const { return recording_state_ == RecordState::kNotStarted; } + + // Returns true if recording can be stopped. + bool CanStop() const { return recording_state_ == RecordState::kRunning; } + + // Returns the filesystem path of the video recording. + std::string GetRecordPath() const { return file_path_; } + + // Returns the duration of the video recording in microseconds. + uint64_t GetRecordedDuration() const { return recording_duration_us_; } + + // Calculates new recording time from capture timestamp. + void UpdateRecordingTime(uint64_t timestamp); + + // Returns true if recording time has exceeded the maximum duration for timed + // recordings. + bool ShouldStopTimedRecording() const; + + private: + // Initializes record sink for video file capture. + HRESULT InitRecordSink(IMFCaptureEngine* capture_engine, + IMFMediaType* base_media_type); + + bool record_audio_ = false; + int64_t max_video_duration_ms_ = -1; + int64_t recording_start_timestamp_us_ = -1; + uint64_t recording_duration_us_ = 0; + std::string file_path_; + RecordState recording_state_ = RecordState::kNotStarted; + RecordingType type_; + ComPtr record_sink_; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_RECORD_HANDLER_H_ diff --git a/packages/camera/camera_windows/windows/string_utils.cpp b/packages/camera/camera_windows/windows/string_utils.cpp new file mode 100644 index 000000000000..2e60e1bb01a7 --- /dev/null +++ b/packages/camera/camera_windows/windows/string_utils.cpp @@ -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. + +#include "string_utils.h" + +#include +#include + +#include + +namespace camera_windows { + +// Converts the given UTF-16 string to UTF-8. +std::string Utf8FromUtf16(const std::wstring& utf16_string) { + if (utf16_string.empty()) { + return std::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; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), + static_cast(utf16_string.length()), utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} + +// 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 camera_windows diff --git a/packages/camera/camera_windows/windows/string_utils.h b/packages/camera/camera_windows/windows/string_utils.h new file mode 100644 index 000000000000..562c46a0feea --- /dev/null +++ b/packages/camera/camera_windows/windows/string_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 PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_STRING_UTILS_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_STRING_UTILS_H_ + +#include + +#include + +namespace camera_windows { + +// Converts the given UTF-16 string to UTF-8. +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); + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_STRING_UTILS_H_ diff --git a/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp new file mode 100644 index 000000000000..309268a1fb90 --- /dev/null +++ b/packages/camera/camera_windows/windows/test/camera_plugin_test.cpp @@ -0,0 +1,1010 @@ +// Copyright 2013 The Flutter 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 "camera_plugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mocks.h" + +namespace camera_windows { +namespace test { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using ::testing::_; +using ::testing::DoAll; +using ::testing::EndsWith; +using ::testing::Eq; +using ::testing::Pointee; +using ::testing::Return; + +TEST(CameraPlugin, AvailableCamerasHandlerSuccessIfNoCameras) { + std::unique_ptr texture_registrar_ = + std::make_unique(); + std::unique_ptr messenger_ = + std::make_unique(); + std::unique_ptr camera_factory_ = + std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + MockCameraPlugin plugin(texture_registrar_.get(), messenger_.get(), + std::move(camera_factory_)); + + EXPECT_CALL(plugin, EnumerateVideoCaptureDeviceSources) + .Times(1) + .WillOnce([](IMFActivate*** devices, UINT32* count) { + *count = 0U; + *devices = static_cast( + CoTaskMemAlloc(sizeof(IMFActivate*) * (*count))); + return true; + }); + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal).Times(1); + + plugin.HandleMethodCall( + flutter::MethodCall("availableCameras", + std::make_unique()), + std::move(result)); +} + +TEST(CameraPlugin, AvailableCamerasHandlerErrorIfFailsToEnumerateDevices) { + std::unique_ptr texture_registrar_ = + std::make_unique(); + std::unique_ptr messenger_ = + std::make_unique(); + std::unique_ptr camera_factory_ = + std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + MockCameraPlugin plugin(texture_registrar_.get(), messenger_.get(), + std::move(camera_factory_)); + + EXPECT_CALL(plugin, EnumerateVideoCaptureDeviceSources) + .Times(1) + .WillOnce([](IMFActivate*** devices, UINT32* count) { return false; }); + + EXPECT_CALL(*result, ErrorInternal).Times(1); + EXPECT_CALL(*result, SuccessInternal).Times(0); + + plugin.HandleMethodCall( + flutter::MethodCall("availableCameras", + std::make_unique()), + std::move(result)); +} + +TEST(CameraPlugin, CreateHandlerCallsInitCamera) { + std::unique_ptr 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(); + 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)); + }); + + // Move mocked camera to the factory to be passed + // for plugin with CreateCamera function. + camera_factory_->pending_camera_ = std::move(camera); + + EXPECT_CALL(*camera_factory_, CreateCamera(MOCK_DEVICE_ID)); + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(1)))); + + 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(result)); +} + +TEST(CameraPlugin, CreateHandlerErrorOnInvalidDeviceId) { + std::unique_ptr 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(); + + CameraPlugin plugin(texture_registrar_.get(), messenger_.get(), + std::move(camera_factory_)); + EncodableMap args = { + {EncodableValue("cameraName"), EncodableValue(MOCK_INVALID_CAMERA_NAME)}, + {EncodableValue("resolutionPreset"), EncodableValue(nullptr)}, + {EncodableValue("enableAudio"), EncodableValue(true)}, + }; + + EXPECT_CALL(*result, ErrorInternal).Times(1); + + plugin.HandleMethodCall( + flutter::MethodCall("create", + std::make_unique(EncodableMap(args))), + std::move(result)); +} + +TEST(CameraPlugin, CreateHandlerErrorOnExistingDeviceId) { + 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(); + 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; + }); + + // Move mocked camera to the factory to be passed + // for plugin with CreateCamera function. + camera_factory_->pending_camera_ = std::move(camera); + + EXPECT_CALL(*camera_factory_, CreateCamera(MOCK_DEVICE_ID)); + + EXPECT_CALL(*first_create_result, ErrorInternal).Times(0); + EXPECT_CALL(*first_create_result, + SuccessInternal(Pointee(EncodableValue(1)))); + + 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(1); + EXPECT_CALL(*second_create_result, SuccessInternal).Times(0); + + plugin.HandleMethodCall( + flutter::MethodCall("create", + std::make_unique(EncodableMap(args))), + std::move(second_create_result)); +} + +TEST(CameraPlugin, InitializeHandlerCallStartPreview) { + int64_t mock_camera_id = 1234; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kInitialize))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, AddPendingResult(Eq(PendingResultType::kInitialize), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, StartPreview()) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("initialize", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, InitializeHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, StartPreview).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(1); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(0); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(missing_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("initialize", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, TakePictureHandlerCallsTakePictureWithPath) { + int64_t mock_camera_id = 1234; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kTakePicture))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, AddPendingResult(Eq(PendingResultType::kTakePicture), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, TakePicture(EndsWith(".jpeg"))) + .Times(1) + .WillOnce([cam = camera.get()](const std::string& file_path) { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("takePicture", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, TakePictureHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, TakePicture).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(1); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(0); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(missing_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("takePicture", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, StartVideoRecordingHandlerCallsStartRecordWithPath) { + int64_t mock_camera_id = 1234; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kStartRecord))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, AddPendingResult(Eq(PendingResultType::kStartRecord), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, StartRecord(EndsWith(".mp4"), -1)) + .Times(1) + .WillOnce([cam = camera.get()](const std::string& file_path, + int64_t max_video_duration_ms) { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("startVideoRecording", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, + StartVideoRecordingHandlerCallsStartRecordWithPathAndCaptureDuration) { + int64_t mock_camera_id = 1234; + int32_t mock_video_duration = 100000; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kStartRecord))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, AddPendingResult(Eq(PendingResultType::kStartRecord), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, + StartRecord(EndsWith(".mp4"), Eq(mock_video_duration))) + .Times(1) + .WillOnce([cam = camera.get()](const std::string& file_path, + int64_t max_video_duration_ms) { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + {EncodableValue("maxVideoDuration"), EncodableValue(mock_video_duration)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("startVideoRecording", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, StartVideoRecordingHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, StartRecord(_, -1)).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(1); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(0); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(missing_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("startVideoRecording", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, StopVideoRecordingHandlerCallsStopRecord) { + int64_t mock_camera_id = 1234; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kStopRecord))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, AddPendingResult(Eq(PendingResultType::kStopRecord), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, StopRecord) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("stopVideoRecording", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, StopVideoRecordingHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, StopRecord).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(1); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(0); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(missing_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("stopVideoRecording", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, ResumePreviewHandlerCallsResumePreview) { + int64_t mock_camera_id = 1234; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kResumePreview))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, + AddPendingResult(Eq(PendingResultType::kResumePreview), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, ResumePreview) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("resumePreview", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, ResumePreviewHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, ResumePreview).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(1); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(0); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(missing_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("resumePreview", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, PausePreviewHandlerCallsPausePreview) { + int64_t mock_camera_id = 1234; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId(Eq(mock_camera_id))) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, + HasPendingResultByType(Eq(PendingResultType::kPausePreview))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_CALL(*camera, + AddPendingResult(Eq(PendingResultType::kPausePreview), _)) + .Times(1) + .WillOnce([cam = camera.get()](PendingResultType type, + std::unique_ptr> result) { + cam->pending_result_ = std::move(result); + return true; + }); + + EXPECT_CALL(*camera, GetCaptureController) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->capture_controller_.get(); + }); + + EXPECT_CALL(*capture_controller, PausePreview) + .Times(1) + .WillOnce([cam = camera.get()]() { + assert(cam->pending_result_); + return cam->pending_result_->Success(); + }); + + camera->camera_id_ = mock_camera_id; + camera->capture_controller_ = std::move(capture_controller); + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(0); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(1); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(mock_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("pausePreview", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +TEST(CameraPlugin, PausePreviewHandlerErrorOnInvalidCameraId) { + int64_t mock_camera_id = 1234; + int64_t missing_camera_id = 5678; + + std::unique_ptr initialize_result = + std::make_unique(); + + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + + std::unique_ptr capture_controller = + std::make_unique(); + + EXPECT_CALL(*camera, HasCameraId) + .Times(1) + .WillOnce([cam = camera.get()](int64_t camera_id) { + return cam->camera_id_ == camera_id; + }); + + EXPECT_CALL(*camera, HasPendingResultByType).Times(0); + EXPECT_CALL(*camera, AddPendingResult).Times(0); + EXPECT_CALL(*camera, GetCaptureController).Times(0); + EXPECT_CALL(*capture_controller, PausePreview).Times(0); + + camera->camera_id_ = mock_camera_id; + + MockCameraPlugin plugin(std::make_unique().get(), + std::make_unique().get(), + std::make_unique()); + + // Add mocked camera to plugins camera list. + plugin.AddCamera(std::move(camera)); + + EXPECT_CALL(*initialize_result, ErrorInternal).Times(1); + EXPECT_CALL(*initialize_result, SuccessInternal).Times(0); + + EncodableMap args = { + {EncodableValue("cameraId"), EncodableValue(missing_camera_id)}, + }; + + plugin.HandleMethodCall( + flutter::MethodCall("pausePreview", + std::make_unique(EncodableMap(args))), + std::move(initialize_result)); +} + +} // namespace test +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/test/camera_test.cpp b/packages/camera/camera_windows/windows/test/camera_test.cpp new file mode 100644 index 000000000000..899c1fdaea62 --- /dev/null +++ b/packages/camera/camera_windows/windows/test/camera_test.cpp @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter 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 "camera.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mocks.h" + +namespace camera_windows { +using ::testing::_; +using ::testing::Eq; +using ::testing::NiceMock; +using ::testing::Pointee; + +namespace test { + +TEST(Camera, InitCameraCreatesCaptureController) { + 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( + []() { return std::make_unique>(); }); + + 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); + + EXPECT_TRUE(camera->GetCaptureController() != nullptr); +} + +TEST(Camera, AddPendingResultReturnsErrorForDuplicates) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr first_pending_result = + std::make_unique(); + std::unique_ptr second_pending_result = + std::make_unique(); + + EXPECT_CALL(*first_pending_result, ErrorInternal).Times(0); + EXPECT_CALL(*first_pending_result, SuccessInternal); + EXPECT_CALL(*second_pending_result, ErrorInternal).Times(1); + + camera->AddPendingResult(PendingResultType::kCreateCamera, + std::move(first_pending_result)); + + // This should fail + camera->AddPendingResult(PendingResultType::kCreateCamera, + std::move(second_pending_result)); + + // Mark pending result as succeeded + camera->OnCreateCaptureEngineSucceeded(0); +} + +TEST(Camera, OnCreateCaptureEngineSucceededReturnsCameraId) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const int64_t texture_id = 12345; + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL( + *result, + SuccessInternal(Pointee(EncodableValue(EncodableMap( + {{EncodableValue("cameraId"), EncodableValue(texture_id)}}))))); + + camera->AddPendingResult(PendingResultType::kCreateCamera, std::move(result)); + + camera->OnCreateCaptureEngineSucceeded(texture_id); +} + +TEST(Camera, OnCreateCaptureEngineFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kCreateCamera, std::move(result)); + + camera->OnCreateCaptureEngineFailed(error_text); +} + +TEST(Camera, OnStartPreviewSucceededReturnsFrameSize) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + const int32_t width = 123; + const int32_t height = 456; + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL( + *result, + SuccessInternal(Pointee(EncodableValue(EncodableMap({ + {EncodableValue("previewWidth"), EncodableValue((float)width)}, + {EncodableValue("previewHeight"), EncodableValue((float)height)}, + }))))); + + camera->AddPendingResult(PendingResultType::kInitialize, std::move(result)); + + camera->OnStartPreviewSucceeded(width, height); +} + +TEST(Camera, OnStartPreviewFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kInitialize, std::move(result)); + + camera->OnStartPreviewFailed(error_text); +} + +TEST(Camera, OnPausePreviewSucceededReturnsSuccess) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal(nullptr)); + + camera->AddPendingResult(PendingResultType::kPausePreview, std::move(result)); + + camera->OnPausePreviewSucceeded(); +} + +TEST(Camera, OnPausePreviewFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kPausePreview, std::move(result)); + + camera->OnPausePreviewFailed(error_text); +} + +TEST(Camera, OnResumePreviewSucceededReturnsSuccess) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal(nullptr)); + + camera->AddPendingResult(PendingResultType::kResumePreview, + std::move(result)); + + camera->OnResumePreviewSucceeded(); +} + +TEST(Camera, OnResumePreviewFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kResumePreview, + std::move(result)); + + camera->OnResumePreviewFailed(error_text); +} + +TEST(Camera, OnStartRecordSucceededReturnsSuccess) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal(nullptr)); + + camera->AddPendingResult(PendingResultType::kStartRecord, std::move(result)); + + camera->OnStartRecordSucceeded(); +} + +TEST(Camera, OnStartRecordFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kStartRecord, std::move(result)); + + camera->OnStartRecordFailed(error_text); +} + +TEST(Camera, OnStopRecordSucceededReturnsSuccess) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string file_path = "C:\temp\filename.mp4"; + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(file_path)))); + + camera->AddPendingResult(PendingResultType::kStopRecord, std::move(result)); + + camera->OnStopRecordSucceeded(file_path); +} + +TEST(Camera, OnStopRecordFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kStopRecord, std::move(result)); + + camera->OnStopRecordFailed(error_text); +} + +TEST(Camera, OnTakePictureSucceededReturnsSuccess) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string file_path = "C:\temp\filename.jpeg"; + + EXPECT_CALL(*result, ErrorInternal).Times(0); + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(file_path)))); + + camera->AddPendingResult(PendingResultType::kTakePicture, std::move(result)); + + camera->OnTakePictureSucceeded(file_path); +} + +TEST(Camera, OnTakePictureFailedReturnsError) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr result = + std::make_unique(); + + std::string error_text = "error_text"; + + EXPECT_CALL(*result, SuccessInternal).Times(0); + EXPECT_CALL(*result, ErrorInternal(_, Eq(error_text), _)); + + camera->AddPendingResult(PendingResultType::kTakePicture, std::move(result)); + + camera->OnTakePictureFailed(error_text); +} + +TEST(Camera, OnVideoRecordSucceededInvokesCameraChannelEvent) { + std::unique_ptr camera = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller_factory = + std::make_unique(); + + std::unique_ptr binary_messenger = + std::make_unique(); + + std::string file_path = "C:\temp\filename.mp4"; + 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; + + EXPECT_CALL(*capture_controller_factory, CreateCaptureController) + .Times(1) + .WillOnce( + []() { return std::make_unique>(); }); + + // TODO: test binary content. + // First time is video record success message, + // and second is camera closing message. + EXPECT_CALL(*binary_messenger, Send(Eq(camera_channel), _, _, _)).Times(2); + + // Init camera with mock capture controller factory + camera->InitCamera(std::move(capture_controller_factory), + std::make_unique().get(), + binary_messenger.get(), false, ResolutionPreset::kAuto); + + // Pass camera id for camera + camera->OnCreateCaptureEngineSucceeded(camera_id); + + camera->OnVideoRecordSucceeded(file_path, video_duration); + + // Dispose camera before message channel. + camera = nullptr; +} + +} // namespace test +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/test/capture_controller_test.cpp b/packages/camera/camera_windows/windows/test/capture_controller_test.cpp new file mode 100644 index 000000000000..7520af7a4af8 --- /dev/null +++ b/packages/camera/camera_windows/windows/test/capture_controller_test.cpp @@ -0,0 +1,503 @@ +// Copyright 2013 The Flutter 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 "capture_controller.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mocks.h" +#include "string_utils.h" + +namespace camera_windows { + +namespace test { + +using Microsoft::WRL::ComPtr; +using ::testing::_; +using ::testing::Eq; +using ::testing::Return; + +void MockInitCaptureController(CaptureControllerImpl* capture_controller, + MockTextureRegistrar* texture_registrar, + MockCaptureEngine* engine, MockCamera* camera, + int64_t mock_texture_id) { + ComPtr video_source = new MockMediaSource(); + ComPtr audio_source = new MockMediaSource(); + + capture_controller->SetCaptureEngine( + reinterpret_cast(engine)); + capture_controller->SetVideoSource( + reinterpret_cast(video_source.Get())); + capture_controller->SetAudioSource( + reinterpret_cast(audio_source.Get())); + + EXPECT_CALL(*texture_registrar, RegisterTexture) + .Times(1) + .WillOnce([reg = texture_registrar, + mock_texture_id](flutter::TextureVariant* texture) -> int64_t { + EXPECT_TRUE(texture); + reg->texture_ = texture; + reg->texture_id_ = mock_texture_id; + return reg->texture_id_; + }); + EXPECT_CALL(*texture_registrar, UnregisterTexture(Eq(mock_texture_id))) + .Times(1); + EXPECT_CALL(*camera, OnCreateCaptureEngineFailed).Times(0); + EXPECT_CALL(*camera, OnCreateCaptureEngineSucceeded(Eq(mock_texture_id))) + .Times(1); + EXPECT_CALL(*engine, Initialize).Times(1); + + capture_controller->InitCaptureDevice(texture_registrar, MOCK_DEVICE_ID, true, + ResolutionPreset::kAuto); + + // MockCaptureEngine::Initialize is called + EXPECT_TRUE(engine->initialized_); + + 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; + }); + + EXPECT_CALL(*engine, GetSource) + .Times(1) + .WillOnce( + [src_source = capture_source](IMFCaptureSource** target_source) { + *target_source = src_source; + src_source->AddRef(); + return S_OK; + }); + + EXPECT_CALL( + *capture_source, + GetAvailableDeviceMediaType( + Eq((DWORD) + MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_PREVIEW), + _, _)) + .WillRepeatedly([mock_preview_width, mock_preview_height]( + DWORD stream_index, DWORD media_type_index, + IMFMediaType** media_type) { + // We give only one media type to loop through + if (media_type_index != 0) return MF_E_NO_MORE_TYPES; + *media_type = + new FakeMediaType(MFMediaType_Video, MFVideoFormat_RGB32, + mock_preview_width, mock_preview_height); + (*media_type)->AddRef(); + return S_OK; + }); + + EXPECT_CALL( + *capture_source, + GetAvailableDeviceMediaType( + Eq((DWORD)MF_CAPTURE_ENGINE_PREFERRED_SOURCE_STREAM_FOR_VIDEO_RECORD), + _, _)) + .WillRepeatedly([mock_preview_width, mock_preview_height]( + DWORD stream_index, DWORD media_type_index, + IMFMediaType** media_type) { + // We give only one media type to loop through + if (media_type_index != 0) return MF_E_NO_MORE_TYPES; + *media_type = + new FakeMediaType(MFMediaType_Video, MFVideoFormat_RGB32, + mock_preview_width, mock_preview_height); + (*media_type)->AddRef(); + return S_OK; + }); + + EXPECT_CALL(*engine, StartPreview()).Times(1).WillOnce(Return(S_OK)); + + // Called by destructor + EXPECT_CALL(*engine, StopPreview()).Times(1).WillOnce(Return(S_OK)); + + // Called after first processed sample + EXPECT_CALL(*camera, + OnStartPreviewSucceeded(mock_preview_width, mock_preview_height)) + .Times(1); + EXPECT_CALL(*camera, OnStartPreviewFailed).Times(0); + EXPECT_CALL(*texture_registrar, MarkTextureFrameAvailable(mock_texture_id)) + .Times(1); + + capture_controller->StartPreview(); + + EXPECT_EQ(capture_controller->GetPreviewHeight(), mock_preview_height); + EXPECT_EQ(capture_controller->GetPreviewWidth(), mock_preview_width); + + // Capture engine is now started and will first send event of started preview + engine->CreateFakeEvent(S_OK, MF_CAPTURE_ENGINE_PREVIEW_STARTED); + + // SendFake sample + preview_sink->SendFakeSample(mock_source_buffer.get(), + mock_source_buffer_size); +} + +void MockRecordStart(CaptureControllerImpl* capture_controller, + MockCaptureEngine* engine, + MockCaptureRecordSink* record_sink, MockCamera* camera, + const std::string& mock_path_to_video) { + EXPECT_CALL(*engine, StartRecord()).Times(1).WillOnce(Return(S_OK)); + + EXPECT_CALL(*engine, 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; + src_sink->AddRef(); + return S_OK; + }); + + EXPECT_CALL(*record_sink, RemoveAllStreams).Times(1).WillOnce(Return(S_OK)); + EXPECT_CALL(*record_sink, AddStream).Times(2).WillRepeatedly(Return(S_OK)); + EXPECT_CALL(*record_sink, SetOutputFileName).Times(1).WillOnce(Return(S_OK)); + + capture_controller->StartRecord(mock_path_to_video, -1); + + EXPECT_CALL(*camera, OnStartRecordSucceeded()).Times(1); + engine->CreateFakeEvent(S_OK, MF_CAPTURE_ENGINE_RECORD_STARTED); +} + +TEST(CaptureController, + InitCaptureEngineCallsOnCreateCaptureEngineSucceededWithTextureId) { + 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(); + + uint64_t mock_texture_id = 1234; + + // Init capture controller with mocks and tests + MockInitCaptureController(capture_controller.get(), texture_registrar.get(), + engine.Get(), camera.get(), mock_texture_id); + + capture_controller = nullptr; + camera = nullptr; + texture_registrar = nullptr; + engine = nullptr; +} + +TEST(CaptureController, StartPreviewStartsProcessingSamples) { + 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(); + + uint64_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. + uint32_t mock_preview_width = 2; + uint32_t mock_preview_height = 1; + uint32_t pixels_total = mock_preview_width * mock_preview_height; + uint32_t pixel_size = 4; + + // Build mock texture + uint32_t mock_texture_data_size = pixels_total * pixel_size; + + std::unique_ptr mock_source_buffer = + std::make_unique(mock_texture_data_size); + + uint8_t mock_red_pixel = 0x11; + uint8_t mock_green_pixel = 0x22; + uint8_t mock_blue_pixel = 0x33; + MFVideoFormatRGB32Pixel* mock_source_buffer_data = + (MFVideoFormatRGB32Pixel*)mock_source_buffer.get(); + + for (uint32_t i = 0; i < pixels_total; i++) { + mock_source_buffer_data[i].r = mock_red_pixel; + mock_source_buffer_data[i].g = mock_green_pixel; + mock_source_buffer_data[i].b = mock_blue_pixel; + } + + // 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); + + // Test texture processing + EXPECT_TRUE(texture_registrar->texture_); + if (texture_registrar->texture_) { + auto pixel_buffer_texture = + std::get_if(texture_registrar->texture_); + EXPECT_TRUE(pixel_buffer_texture); + + if (pixel_buffer_texture) { + auto converted_buffer = + pixel_buffer_texture->CopyPixelBuffer((size_t)100, (size_t)100); + + EXPECT_TRUE(converted_buffer); + if (converted_buffer) { + EXPECT_EQ(converted_buffer->height, mock_preview_height); + EXPECT_EQ(converted_buffer->width, mock_preview_width); + + FlutterDesktopPixel* converted_buffer_data = + (FlutterDesktopPixel*)(converted_buffer->buffer); + + for (uint32_t i = 0; i < pixels_total; i++) { + EXPECT_EQ(converted_buffer_data[i].r, mock_red_pixel); + EXPECT_EQ(converted_buffer_data[i].g, mock_green_pixel); + EXPECT_EQ(converted_buffer_data[i].b, mock_blue_pixel); + } + + // Call release callback to get mutex lock unlocked. + converted_buffer->release_callback(converted_buffer->release_context); + } + converted_buffer = nullptr; + } + pixel_buffer_texture = nullptr; + } + + 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(); + + uint64_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); + + // 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, 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(); + + uint64_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); + + // 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); + engine->CreateFakeEvent(S_OK, 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(); + + uint64_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); + + // 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)); + + // 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); + + // OnTakePictureSucceeded should be called with mocked file path + EXPECT_CALL(*camera, OnTakePictureSucceeded(Eq(mock_path_to_photo))).Times(1); + engine->CreateFakeEvent(S_OK, 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 = + std::make_unique(MOCK_DEVICE_ID); + std::unique_ptr capture_controller = + std::make_unique(camera.get()); + std::unique_ptr texture_registrar = + std::make_unique(); + + uint64_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); + + EXPECT_CALL(*camera, OnPausePreviewSucceeded()).Times(1); + capture_controller->PausePreview(); + + EXPECT_CALL(*camera, OnResumePreviewSucceeded()).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 new file mode 100644 index 000000000000..0781989e94c2 --- /dev/null +++ b/packages/camera/camera_windows/windows/test/mocks.h @@ -0,0 +1,1015 @@ +// Copyright 2013 The Flutter 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_CAMERA_CAMERA_WINDOWS_WINDOWS_TEST_MOCKS_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TEST_MOCKS_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "camera.h" +#include "camera_plugin.h" +#include "capture_controller.h" +#include "capture_controller_listener.h" +#include "capture_engine_listener.h" + +namespace camera_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 MockBinaryMessenger : public flutter::BinaryMessenger { + public: + ~MockBinaryMessenger() = default; + + MOCK_METHOD(void, Send, + (const std::string& channel, const uint8_t* message, + size_t message_size, flutter::BinaryReply reply), + (const)); + + MOCK_METHOD(void, SetMessageHandler, + (const std::string& channel, + flutter::BinaryMessageHandler handler), + ()); +}; + +class MockTextureRegistrar : public flutter::TextureRegistrar { + public: + MockTextureRegistrar() { + ON_CALL(*this, RegisterTexture) + .WillByDefault([this](flutter::TextureVariant* texture) -> int64_t { + EXPECT_TRUE(texture); + this->texture_ = texture; + this->texture_id_ = 1000; + return this->texture_id_; + }); + + ON_CALL(*this, UnregisterTexture) + .WillByDefault([this](int64_t tid) -> bool { + if (tid == this->texture_id_) { + texture_ = nullptr; + this->texture_id_ = -1; + return true; + } + return false; + }); + + ON_CALL(*this, MarkTextureFrameAvailable) + .WillByDefault([this](int64_t tid) -> bool { + if (tid == this->texture_id_) { + return true; + } + return false; + }); + } + + ~MockTextureRegistrar() { texture_ = nullptr; } + + MOCK_METHOD(int64_t, RegisterTexture, (flutter::TextureVariant * texture), + (override)); + + MOCK_METHOD(bool, UnregisterTexture, (int64_t), (override)); + MOCK_METHOD(bool, MarkTextureFrameAvailable, (int64_t), (override)); + + int64_t texture_id_ = -1; + flutter::TextureVariant* texture_ = nullptr; +}; + +class MockCameraFactory : public CameraFactory { + public: + MockCameraFactory() { + ON_CALL(*this, CreateCamera).WillByDefault([this]() { + assert(this->pending_camera_); + return std::move(this->pending_camera_); + }); + } + + ~MockCameraFactory() = default; + + // Disallow copy and move. + MockCameraFactory(const MockCameraFactory&) = delete; + MockCameraFactory& operator=(const MockCameraFactory&) = delete; + + MOCK_METHOD(std::unique_ptr, CreateCamera, + (const std::string& device_id), (override)); + + std::unique_ptr pending_camera_; +}; + +class MockCamera : public Camera { + public: + MockCamera(const std::string& device_id) + : device_id_(device_id), Camera(device_id){}; + + ~MockCamera() = default; + + // Disallow copy and move. + MockCamera(const MockCamera&) = delete; + MockCamera& operator=(const MockCamera&) = delete; + + MOCK_METHOD(void, OnCreateCaptureEngineSucceeded, (int64_t texture_id), + (override)); + MOCK_METHOD(std::unique_ptr>, GetPendingResultByType, + (PendingResultType type)); + MOCK_METHOD(void, OnCreateCaptureEngineFailed, (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, OnResumePreviewSucceeded, (), (override)); + MOCK_METHOD(void, OnResumePreviewFailed, (const std::string& error), + (override)); + + MOCK_METHOD(void, OnPausePreviewSucceeded, (), (override)); + MOCK_METHOD(void, OnPausePreviewFailed, (const std::string& error), + (override)); + + MOCK_METHOD(void, OnStartRecordSucceeded, (), (override)); + MOCK_METHOD(void, OnStartRecordFailed, (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, OnTakePictureSucceeded, (const std::string& file_path), + (override)); + MOCK_METHOD(void, OnTakePictureFailed, (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(bool, HasDeviceId, (std::string & device_id), (const override)); + MOCK_METHOD(bool, HasCameraId, (int64_t camera_id), (const override)); + + MOCK_METHOD(bool, AddPendingResult, + (PendingResultType type, std::unique_ptr> result), + (override)); + MOCK_METHOD(bool, HasPendingResultByType, (PendingResultType type), + (const override)); + + MOCK_METHOD(camera_windows::CaptureController*, GetCaptureController, (), + (override)); + + MOCK_METHOD(void, InitCamera, + (flutter::TextureRegistrar * texture_registrar, + flutter::BinaryMessenger* messenger, bool record_audio, + ResolutionPreset resolution_preset), + (override)); + + std::unique_ptr capture_controller_; + std::unique_ptr> pending_result_; + std::string device_id_; + int64_t camera_id_ = -1; +}; + +class MockCaptureControllerFactory : public CaptureControllerFactory { + public: + MockCaptureControllerFactory(){}; + virtual ~MockCaptureControllerFactory() = default; + + // Disallow copy and move. + MockCaptureControllerFactory(const MockCaptureControllerFactory&) = delete; + MockCaptureControllerFactory& operator=(const MockCaptureControllerFactory&) = + delete; + + MOCK_METHOD(std::unique_ptr, CreateCaptureController, + (CaptureControllerListener * listener), (override)); +}; + +class MockCaptureController : public CaptureController { + public: + ~MockCaptureController() = default; + + MOCK_METHOD(void, InitCaptureDevice, + (flutter::TextureRegistrar * texture_registrar, + const std::string& device_id, bool record_audio, + ResolutionPreset resolution_preset), + (override)); + + MOCK_METHOD(uint32_t, GetPreviewWidth, (), (const override)); + MOCK_METHOD(uint32_t, GetPreviewHeight, (), (const override)); + + // Actions + MOCK_METHOD(void, StartPreview, (), (override)); + MOCK_METHOD(void, ResumePreview, (), (override)); + MOCK_METHOD(void, PausePreview, (), (override)); + MOCK_METHOD(void, StartRecord, + (const std::string& file_path, int64_t max_video_duration_ms), + (override)); + MOCK_METHOD(void, StopRecord, (), (override)); + MOCK_METHOD(void, TakePicture, (const std::string& file_path), (override)); +}; + +// MockCameraPlugin extends CameraPlugin behaviour a bit to allow adding cameras +// without creating them first with create message handler and mocking static +// system calls +class MockCameraPlugin : public CameraPlugin { + public: + MockCameraPlugin(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger) + : CameraPlugin(texture_registrar, messenger){}; + + // Creates a plugin instance with the given CameraFactory instance. + // Exists for unit testing with mock implementations. + MockCameraPlugin(flutter::TextureRegistrar* texture_registrar, + flutter::BinaryMessenger* messenger, + std::unique_ptr camera_factory) + : CameraPlugin(texture_registrar, messenger, std::move(camera_factory)){}; + + ~MockCameraPlugin() = default; + + // Disallow copy and move. + MockCameraPlugin(const MockCameraPlugin&) = delete; + MockCameraPlugin& operator=(const MockCameraPlugin&) = delete; + + MOCK_METHOD(bool, EnumerateVideoCaptureDeviceSources, + (IMFActivate * **devices, UINT32* count), (override)); + + // Helper to add camera without creating it via CameraFactory for testing + // purposes + void AddCamera(std::unique_ptr camera) { + cameras_.push_back(std::move(camera)); + } +}; + +class MockCaptureSource : public IMFCaptureSource { + public: + MockCaptureSource(){}; + ~MockCaptureSource() = default; + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFCaptureSource) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + MOCK_METHOD(HRESULT, GetCaptureDeviceSource, + (MF_CAPTURE_ENGINE_DEVICE_TYPE mfCaptureEngineDeviceType, + IMFMediaSource** ppMediaSource)); + MOCK_METHOD(HRESULT, GetCaptureDeviceActivate, + (MF_CAPTURE_ENGINE_DEVICE_TYPE mfCaptureEngineDeviceType, + IMFActivate** ppActivate)); + MOCK_METHOD(HRESULT, GetService, + (REFIID rguidService, REFIID riid, IUnknown** ppUnknown)); + MOCK_METHOD(HRESULT, AddEffect, + (DWORD dwSourceStreamIndex, IUnknown* pUnknown)); + + MOCK_METHOD(HRESULT, RemoveEffect, + (DWORD dwSourceStreamIndex, IUnknown* pUnknown)); + MOCK_METHOD(HRESULT, RemoveAllEffects, (DWORD dwSourceStreamIndex)); + MOCK_METHOD(HRESULT, GetAvailableDeviceMediaType, + (DWORD dwSourceStreamIndex, DWORD dwMediaTypeIndex, + IMFMediaType** ppMediaType)); + MOCK_METHOD(HRESULT, SetCurrentDeviceMediaType, + (DWORD dwSourceStreamIndex, IMFMediaType* pMediaType)); + MOCK_METHOD(HRESULT, GetCurrentDeviceMediaType, + (DWORD dwSourceStreamIndex, IMFMediaType** ppMediaType)); + MOCK_METHOD(HRESULT, GetDeviceStreamCount, (DWORD * pdwStreamCount)); + MOCK_METHOD(HRESULT, GetDeviceStreamCategory, + (DWORD dwSourceStreamIndex, + MF_CAPTURE_ENGINE_STREAM_CATEGORY* pStreamCategory)); + MOCK_METHOD(HRESULT, GetMirrorState, + (DWORD dwStreamIndex, BOOL* pfMirrorState)); + MOCK_METHOD(HRESULT, SetMirrorState, + (DWORD dwStreamIndex, BOOL fMirrorState)); + MOCK_METHOD(HRESULT, GetStreamIndexFromFriendlyName, + (UINT32 uifriendlyName, DWORD* pdwActualStreamIndex)); + + private: + volatile ULONG ref_ = 0; +}; + +// Uses IMFMediaSourceEx which has SetD3DManager method. +class MockMediaSource : public IMFMediaSourceEx { + public: + MockMediaSource(){}; + ~MockMediaSource() = default; + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFMediaSource) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + // IMFMediaSource + HRESULT GetCharacteristics(DWORD* dwCharacteristics) override { + return E_NOTIMPL; + } + // IMFMediaSource + HRESULT CreatePresentationDescriptor( + IMFPresentationDescriptor** presentationDescriptor) override { + return E_NOTIMPL; + } + // IMFMediaSource + HRESULT Start(IMFPresentationDescriptor* presentationDescriptor, + const GUID* guidTimeFormat, + const PROPVARIANT* varStartPosition) override { + return E_NOTIMPL; + } + // IMFMediaSource + HRESULT Stop(void) override { return E_NOTIMPL; } + // IMFMediaSource + HRESULT Pause(void) override { return E_NOTIMPL; } + // IMFMediaSource + HRESULT Shutdown(void) override { return E_NOTIMPL; } + + // IMFMediaEventGenerator + HRESULT GetEvent(DWORD dwFlags, IMFMediaEvent** event) override { + return E_NOTIMPL; + } + // IMFMediaEventGenerator + HRESULT BeginGetEvent(IMFAsyncCallback* callback, + IUnknown* unkState) override { + return E_NOTIMPL; + } + // IMFMediaEventGenerator + HRESULT EndGetEvent(IMFAsyncResult* result, IMFMediaEvent** event) override { + return E_NOTIMPL; + } + // IMFMediaEventGenerator + HRESULT QueueEvent(MediaEventType met, REFGUID guidExtendedType, + HRESULT hrStatus, const PROPVARIANT* value) override { + return E_NOTIMPL; + } + + // IMFMediaSourceEx + HRESULT GetSourceAttributes(IMFAttributes** attributes) { return E_NOTIMPL; } + // IMFMediaSourceEx + HRESULT GetStreamAttributes(DWORD stream_id, IMFAttributes** attributes) { + return E_NOTIMPL; + } + // IMFMediaSourceEx + HRESULT SetD3DManager(IUnknown* manager) { return S_OK; } + + private: + volatile ULONG ref_ = 0; +}; + +class MockCapturePreviewSink : public IMFCapturePreviewSink { + public: + // IMFCaptureSink + MOCK_METHOD(HRESULT, GetOutputMediaType, + (DWORD dwSinkStreamIndex, IMFMediaType** ppMediaType)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, GetService, + (DWORD dwSinkStreamIndex, REFGUID rguidService, REFIID riid, + IUnknown** ppUnknown)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, AddStream, + (DWORD dwSourceStreamIndex, IMFMediaType* pMediaType, + IMFAttributes* pAttributes, DWORD* pdwSinkStreamIndex)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, Prepare, ()); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, RemoveAllStreams, ()); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, SetRenderHandle, (HANDLE handle)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, SetRenderSurface, (IUnknown * pSurface)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, UpdateVideo, + (const MFVideoNormalizedRect* pSrc, const RECT* pDst, + const COLORREF* pBorderClr)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, SetSampleCallback, + (DWORD dwStreamSinkIndex, + IMFCaptureEngineOnSampleCallback* pCallback)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, GetMirrorState, (BOOL * pfMirrorState)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, SetMirrorState, (BOOL fMirrorState)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, GetRotation, + (DWORD dwStreamIndex, DWORD* pdwRotationValue)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, SetRotation, + (DWORD dwStreamIndex, DWORD dwRotationValue)); + + // IMFCapturePreviewSink + MOCK_METHOD(HRESULT, SetCustomSink, (IMFMediaSink * pMediaSink)); + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFCapturePreviewSink) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + void SendFakeSample(uint8_t* src_buffer, uint32_t size) { + assert(sample_callback_); + ComPtr sample; + ComPtr buffer; + HRESULT hr = MFCreateSample(&sample); + + if (SUCCEEDED(hr)) { + hr = MFCreateMemoryBuffer(size, &buffer); + } + + if (SUCCEEDED(hr)) { + uint8_t* target_data; + if (SUCCEEDED(buffer->Lock(&target_data, nullptr, nullptr))) { + std::copy(src_buffer, src_buffer + size, target_data); + } + hr = buffer->Unlock(); + } + + if (SUCCEEDED(hr)) { + hr = buffer->SetCurrentLength(size); + } + + if (SUCCEEDED(hr)) { + hr = sample->AddBuffer(buffer.Get()); + } + + if (SUCCEEDED(hr)) { + sample_callback_->OnSample(sample.Get()); + } + } + + ComPtr sample_callback_; + + private: + ~MockCapturePreviewSink() = default; + volatile ULONG ref_ = 0; +}; + +class MockCaptureRecordSink : public IMFCaptureRecordSink { + public: + // IMFCaptureSink + MOCK_METHOD(HRESULT, GetOutputMediaType, + (DWORD dwSinkStreamIndex, IMFMediaType** ppMediaType)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, GetService, + (DWORD dwSinkStreamIndex, REFGUID rguidService, REFIID riid, + IUnknown** ppUnknown)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, AddStream, + (DWORD dwSourceStreamIndex, IMFMediaType* pMediaType, + IMFAttributes* pAttributes, DWORD* pdwSinkStreamIndex)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, Prepare, ()); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, RemoveAllStreams, ()); + + // IMFCaptureRecordSink + MOCK_METHOD(HRESULT, SetOutputByteStream, + (IMFByteStream * pByteStream, REFGUID guidContainerType)); + + // IMFCaptureRecordSink + MOCK_METHOD(HRESULT, SetOutputFileName, (LPCWSTR fileName)); + + // IMFCaptureRecordSink + MOCK_METHOD(HRESULT, SetSampleCallback, + (DWORD dwStreamSinkIndex, + IMFCaptureEngineOnSampleCallback* pCallback)); + + // IMFCaptureRecordSink + MOCK_METHOD(HRESULT, SetCustomSink, (IMFMediaSink * pMediaSink)); + + // IMFCaptureRecordSink + MOCK_METHOD(HRESULT, GetRotation, + (DWORD dwStreamIndex, DWORD* pdwRotationValue)); + + // IMFCaptureRecordSink + MOCK_METHOD(HRESULT, SetRotation, + (DWORD dwStreamIndex, DWORD dwRotationValue)); + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFCaptureRecordSink) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + private: + ~MockCaptureRecordSink() = default; + volatile ULONG ref_ = 0; +}; + +class MockCapturePhotoSink : public IMFCapturePhotoSink { + public: + // IMFCaptureSink + MOCK_METHOD(HRESULT, GetOutputMediaType, + (DWORD dwSinkStreamIndex, IMFMediaType** ppMediaType)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, GetService, + (DWORD dwSinkStreamIndex, REFGUID rguidService, REFIID riid, + IUnknown** ppUnknown)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, AddStream, + (DWORD dwSourceStreamIndex, IMFMediaType* pMediaType, + IMFAttributes* pAttributes, DWORD* pdwSinkStreamIndex)); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, Prepare, ()); + + // IMFCaptureSink + MOCK_METHOD(HRESULT, RemoveAllStreams, ()); + + // IMFCapturePhotoSink + MOCK_METHOD(HRESULT, SetOutputFileName, (LPCWSTR fileName)); + + // IMFCapturePhotoSink + MOCK_METHOD(HRESULT, SetSampleCallback, + (IMFCaptureEngineOnSampleCallback * pCallback)); + + // IMFCapturePhotoSink + MOCK_METHOD(HRESULT, SetOutputByteStream, (IMFByteStream * pByteStream)); + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFCapturePhotoSink) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + private: + ~MockCapturePhotoSink() = default; + volatile ULONG ref_ = 0; +}; + +template +class FakeIMFAttributesBase : public T { + static_assert(std::is_base_of::value, + "I must inherit from IMFAttributes"); + + // IIMFAttributes + HRESULT GetItem(REFGUID guidKey, PROPVARIANT* pValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetItemType(REFGUID guidKey, MF_ATTRIBUTE_TYPE* pType) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT CompareItem(REFGUID guidKey, REFPROPVARIANT Value, + BOOL* pbResult) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT Compare(IMFAttributes* pTheirs, MF_ATTRIBUTES_MATCH_TYPE MatchType, + BOOL* pbResult) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetUINT32(REFGUID guidKey, UINT32* punValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetUINT64(REFGUID guidKey, UINT64* punValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetDouble(REFGUID guidKey, double* pfValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetGUID(REFGUID guidKey, GUID* pguidValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetStringLength(REFGUID guidKey, UINT32* pcchLength) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetString(REFGUID guidKey, LPWSTR pwszValue, UINT32 cchBufSize, + UINT32* pcchLength) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetAllocatedString(REFGUID guidKey, LPWSTR* ppwszValue, + UINT32* pcchLength) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetBlobSize(REFGUID guidKey, UINT32* pcbBlobSize) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetBlob(REFGUID guidKey, UINT8* pBuf, UINT32 cbBufSize, + UINT32* pcbBlobSize) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetAllocatedBlob(REFGUID guidKey, UINT8** ppBuf, + UINT32* pcbSize) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT GetUnknown(REFGUID guidKey, REFIID riid, + __RPC__deref_out_opt LPVOID* ppv) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetItem(REFGUID guidKey, REFPROPVARIANT Value) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT DeleteItem(REFGUID guidKey) override { return E_NOTIMPL; } + + // IIMFAttributes + HRESULT DeleteAllItems(void) override { return E_NOTIMPL; } + + // IIMFAttributes + HRESULT SetUINT32(REFGUID guidKey, UINT32 unValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetUINT64(REFGUID guidKey, UINT64 unValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetDouble(REFGUID guidKey, double fValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetGUID(REFGUID guidKey, REFGUID guidValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetString(REFGUID guidKey, LPCWSTR wszValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetBlob(REFGUID guidKey, const UINT8* pBuf, + UINT32 cbBufSize) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT SetUnknown(REFGUID guidKey, IUnknown* pUnknown) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT LockStore(void) override { return E_NOTIMPL; } + + // IIMFAttributes + HRESULT UnlockStore(void) override { return E_NOTIMPL; } + + // IIMFAttributes + HRESULT GetCount(UINT32* pcItems) override { return E_NOTIMPL; } + + // IIMFAttributes + HRESULT GetItemByIndex(UINT32 unIndex, GUID* pguidKey, + PROPVARIANT* pValue) override { + return E_NOTIMPL; + } + + // IIMFAttributes + HRESULT CopyAllItems(IMFAttributes* pDest) override { return E_NOTIMPL; } +}; + +class FakeMediaType : public FakeIMFAttributesBase { + public: + FakeMediaType(GUID major_type, GUID sub_type, int width, int height) + : major_type_(major_type), + sub_type_(sub_type), + width_(width), + height_(height){}; + + // IMFAttributes + HRESULT GetUINT64(REFGUID key, UINT64* value) override { + if (key == MF_MT_FRAME_SIZE) { + *value = (int64_t)width_ << 32 | (int64_t)height_; + return S_OK; + } else if (key == MF_MT_FRAME_RATE) { + *value = (int64_t)frame_rate_ << 32 | 1; + return S_OK; + } + return E_FAIL; + }; + + // IMFAttributes + HRESULT GetGUID(REFGUID key, GUID* value) override { + if (key == MF_MT_MAJOR_TYPE) { + *value = major_type_; + return S_OK; + } else if (key == MF_MT_SUBTYPE) { + *value = sub_type_; + return S_OK; + } + return E_FAIL; + } + + // IIMFAttributes + HRESULT CopyAllItems(IMFAttributes* pDest) override { + pDest->SetUINT64(MF_MT_FRAME_SIZE, + (int64_t)width_ << 32 | (int64_t)height_); + pDest->SetUINT64(MF_MT_FRAME_RATE, (int64_t)frame_rate_ << 32 | 1); + pDest->SetGUID(MF_MT_MAJOR_TYPE, major_type_); + pDest->SetGUID(MF_MT_SUBTYPE, sub_type_); + return S_OK; + } + + // IMFMediaType + HRESULT STDMETHODCALLTYPE GetMajorType(GUID* pguidMajorType) override { + return E_NOTIMPL; + }; + + // IMFMediaType + HRESULT STDMETHODCALLTYPE IsCompressedFormat(BOOL* pfCompressed) override { + return E_NOTIMPL; + } + + // IMFMediaType + HRESULT STDMETHODCALLTYPE IsEqual(IMFMediaType* pIMediaType, + DWORD* pdwFlags) override { + return E_NOTIMPL; + } + + // IMFMediaType + HRESULT STDMETHODCALLTYPE GetRepresentation( + GUID guidRepresentation, LPVOID* ppvRepresentation) override { + return E_NOTIMPL; + } + + // IMFMediaType + HRESULT STDMETHODCALLTYPE FreeRepresentation( + GUID guidRepresentation, LPVOID pvRepresentation) override { + return E_NOTIMPL; + } + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFMediaType) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + private: + ~FakeMediaType() = default; + volatile ULONG ref_ = 0; + const GUID major_type_; + const GUID sub_type_; + const int width_; + const int height_; + const int frame_rate_ = 30; +}; + +class MockCaptureEngine : public IMFCaptureEngine { + public: + MockCaptureEngine() { + ON_CALL(*this, Initialize) + .WillByDefault([this](IMFCaptureEngineOnEventCallback* callback, + IMFAttributes* attributes, IUnknown* audioSource, + IUnknown* videoSource) -> HRESULT { + EXPECT_TRUE(callback); + EXPECT_TRUE(attributes); + EXPECT_TRUE(videoSource); + // audioSource is allowed to be nullptr; + callback_ = callback; + videoSource_ = reinterpret_cast(videoSource); + audioSource_ = reinterpret_cast(audioSource); + initialized_ = true; + return S_OK; + }); + }; + + virtual ~MockCaptureEngine() = default; + + MOCK_METHOD(HRESULT, Initialize, + (IMFCaptureEngineOnEventCallback * callback, + IMFAttributes* attributes, IUnknown* audioSource, + IUnknown* videoSource)); + MOCK_METHOD(HRESULT, StartPreview, ()); + MOCK_METHOD(HRESULT, StopPreview, ()); + MOCK_METHOD(HRESULT, StartRecord, ()); + MOCK_METHOD(HRESULT, StopRecord, + (BOOL finalize, BOOL flushUnprocessedSamples)); + MOCK_METHOD(HRESULT, TakePhoto, ()); + MOCK_METHOD(HRESULT, GetSink, + (MF_CAPTURE_ENGINE_SINK_TYPE type, IMFCaptureSink** sink)); + MOCK_METHOD(HRESULT, GetSource, (IMFCaptureSource * *ppSource)); + + // IUnknown + STDMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&ref_); } + + // IUnknown + STDMETHODIMP_(ULONG) Release() { + LONG ref = InterlockedDecrement(&ref_); + if (ref == 0) { + delete this; + } + return ref; + } + + // IUnknown + STDMETHODIMP_(HRESULT) QueryInterface(const IID& riid, void** ppv) { + *ppv = nullptr; + + if (riid == IID_IMFCaptureEngine) { + *ppv = static_cast(this); + ((IUnknown*)*ppv)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; + } + + void CreateFakeEvent(HRESULT hrStatus, GUID event_type) { + EXPECT_TRUE(initialized_); + ComPtr event; + MFCreateMediaEvent(MEExtendedType, event_type, hrStatus, nullptr, &event); + if (callback_) { + callback_->OnEvent(event.Get()); + } + } + + ComPtr callback_; + ComPtr videoSource_; + ComPtr audioSource_; + volatile ULONG ref_ = 0; + bool initialized_ = false; +}; + +#define MOCK_DEVICE_ID "mock_device_id" +#define MOCK_CAMERA_NAME "mock_camera_name <" MOCK_DEVICE_ID ">" +#define MOCK_INVALID_CAMERA_NAME "invalid_camera_name" + +} // namespace +} // namespace test +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TEST_MOCKS_H_ diff --git a/packages/camera/camera_windows/windows/texture_handler.cpp b/packages/camera/camera_windows/windows/texture_handler.cpp new file mode 100644 index 000000000000..a7c94738698a --- /dev/null +++ b/packages/camera/camera_windows/windows/texture_handler.cpp @@ -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. + +#include "texture_handler.h" + +#include + +namespace camera_windows { + +TextureHandler::~TextureHandler() { + // Texture might still be processed while destructor is called. + // Lock mutex for safe destruction + const std::lock_guard lock(buffer_mutex_); + if (texture_registrar_ && texture_id_ > 0) { + texture_registrar_->UnregisterTexture(texture_id_); + } + texture_id_ = -1; + texture_ = nullptr; + texture_registrar_ = nullptr; +} + +int64_t TextureHandler::RegisterTexture() { + if (!texture_registrar_) { + return -1; + } + + // Create flutter desktop pixelbuffer texture; + texture_ = + std::make_unique(flutter::PixelBufferTexture( + [this](size_t width, + size_t height) -> const FlutterDesktopPixelBuffer* { + return this->ConvertPixelBufferForFlutter(width, height); + })); + + texture_id_ = texture_registrar_->RegisterTexture(texture_.get()); + return texture_id_; +} + +bool TextureHandler::UpdateBuffer(uint8_t* data, uint32_t data_length) { + // Scoped lock guard. + { + const std::lock_guard lock(buffer_mutex_); + if (!TextureRegistered()) { + return false; + } + + if (source_buffer_.size() != data_length) { + // Update source buffer size. + source_buffer_.resize(data_length); + } + std::copy(data, data + data_length, source_buffer_.data()); + } + OnBufferUpdated(); + return true; +}; + +// Marks texture frame available after buffer is updated. +void TextureHandler::OnBufferUpdated() { + if (TextureRegistered()) { + texture_registrar_->MarkTextureFrameAvailable(texture_id_); + } +} + +const FlutterDesktopPixelBuffer* TextureHandler::ConvertPixelBufferForFlutter( + size_t target_width, size_t target_height) { + // TODO: optimize image processing size by adjusting capture size + // dynamically to match target_width and target_height. + // If target size changes, create new media type for preview and set new + // target framesize to MF_MT_FRAME_SIZE attribute. + // Size should be kept inside requested resolution preset. + // Update output media type with IMFCaptureSink2::SetOutputMediaType method + // call and implement IMFCaptureEngineOnSampleCallback2::OnSynchronizedEvent + // to detect size changes. + + // Lock buffer mutex to protect texture processing + std::unique_lock buffer_lock(buffer_mutex_); + if (!TextureRegistered()) { + return nullptr; + } + + const uint32_t bytes_per_pixel = 4; + const uint32_t pixels_total = preview_frame_width_ * preview_frame_height_; + const uint32_t data_size = pixels_total * bytes_per_pixel; + if (data_size > 0 && source_buffer_.size() == data_size) { + if (dest_buffer_.size() != data_size) { + dest_buffer_.resize(data_size); + } + + // Map buffers to structs for easier conversion. + MFVideoFormatRGB32Pixel* src = + reinterpret_cast(source_buffer_.data()); + FlutterDesktopPixel* dst = + reinterpret_cast(dest_buffer_.data()); + + for (uint32_t y = 0; y < preview_frame_height_; y++) { + for (uint32_t x = 0; x < preview_frame_width_; x++) { + uint32_t sp = (y * preview_frame_width_) + x; + if (mirror_preview_) { + // Software mirror mode. + // IMFCapturePreviewSink also has the SetMirrorState setting, + // but if enabled, samples will not be processed. + + // Calculates mirrored pixel position. + uint32_t tp = + (y * preview_frame_width_) + ((preview_frame_width_ - 1) - x); + dst[tp].r = src[sp].r; + dst[tp].g = src[sp].g; + dst[tp].b = src[sp].b; + dst[tp].a = 255; + } else { + dst[sp].r = src[sp].r; + dst[sp].g = src[sp].g; + dst[sp].b = src[sp].b; + dst[sp].a = 255; + } + } + } + + if (!flutter_desktop_pixel_buffer_) { + flutter_desktop_pixel_buffer_ = + std::make_unique(); + + // Unlocks mutex after texture is processed. + flutter_desktop_pixel_buffer_->release_callback = + [](void* release_context) { + auto mutex = reinterpret_cast(release_context); + mutex->unlock(); + }; + } + + flutter_desktop_pixel_buffer_->buffer = dest_buffer_.data(); + flutter_desktop_pixel_buffer_->width = preview_frame_width_; + flutter_desktop_pixel_buffer_->height = preview_frame_height_; + + // Releases unique_lock and set mutex pointer for release context. + flutter_desktop_pixel_buffer_->release_context = buffer_lock.release(); + + return flutter_desktop_pixel_buffer_.get(); + } + return nullptr; +} + +} // namespace camera_windows diff --git a/packages/camera/camera_windows/windows/texture_handler.h b/packages/camera/camera_windows/windows/texture_handler.h new file mode 100644 index 000000000000..b85611c25608 --- /dev/null +++ b/packages/camera/camera_windows/windows/texture_handler.h @@ -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. + +#ifndef PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TEXTURE_HANDLER_H_ +#define PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TEXTURE_HANDLER_H_ + +#include + +#include +#include +#include + +namespace camera_windows { + +// Describes flutter desktop pixelbuffers pixel data order. +struct FlutterDesktopPixel { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + uint8_t a = 0; +}; + +// Describes MFVideoFormat_RGB32 data order. +struct MFVideoFormatRGB32Pixel { + uint8_t b = 0; + uint8_t g = 0; + uint8_t r = 0; + uint8_t x = 0; +}; + +// Handles the registration of Flutter textures, pixel buffers, and the +// conversion of texture formats. +class TextureHandler { + public: + TextureHandler(flutter::TextureRegistrar* texture_registrar) + : texture_registrar_(texture_registrar) {} + virtual ~TextureHandler(); + + // Prevent copying. + TextureHandler(TextureHandler const&) = delete; + TextureHandler& operator=(TextureHandler const&) = delete; + + // Updates source data buffer with given data. + bool UpdateBuffer(uint8_t* data, uint32_t data_length); + + // Registers texture and updates given texture_id pointer value. + int64_t RegisterTexture(); + + // Updates current preview texture size. + void UpdateTextureSize(uint32_t width, uint32_t height) { + preview_frame_width_ = width; + preview_frame_height_ = height; + } + + // Sets software mirror state. + void SetMirrorPreviewState(bool mirror) { mirror_preview_ = mirror; } + + private: + // Informs flutter texture registrar of updated texture. + void OnBufferUpdated(); + + // Converts local pixel buffer to flutter pixel buffer. + const FlutterDesktopPixelBuffer* ConvertPixelBufferForFlutter(size_t width, + size_t height); + + // Checks if texture registrar, texture id and texture are available. + bool TextureRegistered() { + return texture_registrar_ && texture_ && texture_id_ > -1; + } + + bool mirror_preview_ = true; + int64_t texture_id_ = -1; + uint32_t bytes_per_pixel_ = 4; + uint32_t source_buffer_size_ = 0; + uint32_t preview_frame_width_ = 0; + uint32_t preview_frame_height_ = 0; + + std::vector source_buffer_; + std::vector dest_buffer_; + std::unique_ptr texture_; + std::unique_ptr flutter_desktop_pixel_buffer_ = + nullptr; + flutter::TextureRegistrar* texture_registrar_ = nullptr; + + std::mutex buffer_mutex_; +}; + +} // namespace camera_windows + +#endif // PACKAGES_CAMERA_CAMERA_WINDOWS_WINDOWS_TEXTURE_HANDLER_H_ diff --git a/packages/connectivity/analysis_options.yaml b/packages/connectivity/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/connectivity/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md deleted file mode 100644 index 932565842efd..000000000000 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ /dev/null @@ -1,297 +0,0 @@ -## NEXT - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. -* Specify Java 8 for Android build. - -## 3.0.6 - -* Update README to point to Plus Plugins version. - -## 3.0.5 - -* Ignore Reachability pointer to int cast warning. - -## 3.0.4 - -* Migrate maven repository from jcenter to mavenCentral. - -## 3.0.3 - -* Re-endorse connectivity_for_web - -## 3.0.2 - -* Update platform_plugin_interface version requirement. - -## 3.0.1 - -* Migrate tests to null safety. - -## 3.0.0 - -* Migrate to null safety. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) -* Android: Cleanup the NetworkCallback object when a connectivity stream is cancelled - -## 2.0.3 - -* Update Flutter SDK constraint. - -## 2.0.2 - -* Android: Fix IllegalArgumentException. -* Android: Update Example project. - -## 2.0.1 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 2.0.0 - -* [Breaking Change] The `getWifiName`, `getWifiBSSID` and `getWifiIP` are removed to [wifi_info_flutter](https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter) -* Migration guide: - - If you don't use any of the above APIs, your code should work as is. In addition, you can also remove `NSLocationAlwaysAndWhenInUseUsageDescription` and `NSLocationWhenInUseUsageDescription` in `ios/Runner/Info.plist` - - If you use any of the above APIs, you can find the same APIs in the [wifi_info_flutter](https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter) plugin. - For example, to migrate `getWifiName`, use the new plugin: - ```dart - final WifiInfo _wifiInfo = WifiInfo(); - final String wifiName = await _wifiInfo.getWifiName(); - ``` - -## 1.0.0 - -* Mark wifi related code deprecated. -* Announce 1.0.0! - -## 0.4.9+5 - -* Update android compileSdkVersion to 29. - -## 0.4.9+4 - -* Update README with the updated information about WifiInfo on Android O or higher. -* Android: Avoiding uses or overrides a deprecated API - -## 0.4.9+3 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.9+2 - -* Update package:e2e to use package:integration_test - -## 0.4.9+1 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.9 - -* Add support for `web` (by endorsing `connectivity_for_web` 0.3.0) - -## 0.4.8+6 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.8+5 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.8+4 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Clean up various Android workarounds no longer needed after framework v1.12. -* Complete v2 embedding support. -* Fix CocoaPods podspec lint warnings. - -## 0.4.8+3 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.8+2 - -* Remove hard coded ios workspace setting of the example app. - -## 0.4.8+1 - -* Make the pedantic dev_dependency explicit. - -## 0.4.8 - -* Adds macOS as an endorsed platform. - -## 0.4.7 - -* Migrate the plugin to use the ConnectivityPlatform.instance defined in the connectivity_platform_interface package. - -## 0.4.6+2 - -* Migrate deprecated BinaryMessages to ServicesBinding.instance.defaultBinaryMessenger. -* Bump Flutter SDK to 1.12.13+hotfix.5 or greater (current stable). - -## 0.4.6+1 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.6 - -* Add macOS support. - -## 0.4.5+8 - -* Update documentation to explain when connectivity updates are received on Android. - -## 0.4.5+7 - -* Fix unawaited futures in the example app and tests. - -## 0.4.5+6 - -* Fix singleton Reachability problem on iOS. - -## 0.4.5+5 - -* Add an analyzer check for the public documentation. - -## 0.4.5+4 - -* Stability and Maintainability: update documentations. - -## 0.4.5+3 - -* Remove AndroidX warnings. - -## 0.4.5+2 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.5+1 - -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. - -## 0.4.5 - -* Support the v2 Android embedder. - -## 0.4.4+1 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.4.4 - -* Add `requestLocationServiceAuthorization` to request location authorization on iOS. -* Add `getLocationServiceAuthorization` to get location authorization status on iOS. -* Update README: add more information on iOS 13 updates with CNCopyCurrentNetworkInfo. - -## 0.4.3+7 - -* Update README with the updated information about CNCopyCurrentNetworkInfo on iOS 13. - -## 0.4.3+6 - -* [Android] Fix the invalid suppression check (it should be "deprecation" not "deprecated"). - -## 0.4.3+5 - -* [Android] Added API 29 support for `check()`. -* [Android] Suppress warnings for using deprecated APIs. - -## 0.4.3+4 - -* [Android] Updated logic to retrieve network info. - -## 0.4.3+3 - -* Support for TYPE_MOBILE_HIPRI on Android. - -## 0.4.3+2 - -* Add missing template type parameter to `invokeMethod` calls. - -## 0.4.3+1 - -* Fixes lint error by using `getApplicationContext()` when accessing the Wifi Service. - -## 0.4.3 - -* Add getWifiBSSID to obtain current wifi network's BSSID. - -## 0.4.2+2 - -* Add integration test. - -## 0.4.2+1 - -* Bump the minimum Flutter version to 1.2.0. -* Add template type parameter to `invokeMethod` calls. - -## 0.4.2 - -* Adding getWifiIP() to obtain current wifi network's IP. - -## 0.4.1 - -* Add unit tests. - -## 0.4.0+2 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0+1 - -* Updated `Connectivity` to a singleton. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.2 - -* Adding getWifiName() to obtain current wifi network's SSID. - -## 0.3.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.3.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.2.1 - -* Fixed warnings from the Dart 2.0 analyzer. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.2.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.1.1 - -* Add FLT prefix to iOS types. - -## 0.1.0 - -* Breaking API change: Have a Connectivity class instead of a top level function -* Introduce ability to listen for network state changes - -## 0.0.1 - -* Initial release diff --git a/packages/connectivity/connectivity/README.md b/packages/connectivity/connectivity/README.md deleted file mode 100644 index d085c18ba1e4..000000000000 --- a/packages/connectivity/connectivity/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# connectivity - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`connectivity_plus`](https://pub.dev/packages/connectivity_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -This plugin allows Flutter apps to discover network connectivity and configure -themselves accordingly. It can distinguish between cellular vs WiFi connection. -This plugin works for iOS and Android. - -> Note that on Android, this does not guarantee connection to Internet. For instance, -the app might have wifi access but it might be a VPN or a hotel WiFi with no access. - -## Usage - -Sample usage to check current status: - -```dart -import 'package:connectivity/connectivity.dart'; - -var connectivityResult = await (Connectivity().checkConnectivity()); -if (connectivityResult == ConnectivityResult.mobile) { - // I am connected to a mobile network. -} else if (connectivityResult == ConnectivityResult.wifi) { - // I am connected to a wifi network. -} -``` - -> Note that you should not be using the current network status for deciding -whether you can reliably make a network connection. Always guard your app code -against timeouts and errors that might come from the network layer. - -You can also listen for network state changes by subscribing to the stream -exposed by connectivity plugin: - -```dart -import 'package:connectivity/connectivity.dart'; - -@override -initState() { - super.initState(); - - subscription = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { - // Got a new connectivity status! - }) -} - -// Be sure to cancel subscription after you are done -@override -dispose() { - super.dispose(); - - subscription.cancel(); -} -``` - -Note that connectivity changes are no longer communicated to Android apps in the background starting with Android O. *You should always check for connectivity status when your app is resumed.* The broadcast is only useful when your application is in the foreground. - -## 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/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle deleted file mode 100644 index e1ba0c7c892e..000000000000 --- a/packages/connectivity/connectivity/android/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -group 'io.flutter.plugins.connectivity' -version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} diff --git a/packages/connectivity/connectivity/android/settings.gradle b/packages/connectivity/connectivity/android/settings.gradle deleted file mode 100644 index 4fbed4753c9c..000000000000 --- a/packages/connectivity/connectivity/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'connectivity' diff --git a/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml b/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml deleted file mode 100644 index 52bbe9edafa0..000000000000 --- a/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/Connectivity.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/Connectivity.java deleted file mode 100644 index d7e254e84595..000000000000 --- a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/Connectivity.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.connectivity; - -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.os.Build; - -/** Reports connectivity related information such as connectivity type and wifi information. */ -public class Connectivity { - private ConnectivityManager connectivityManager; - - public Connectivity(ConnectivityManager connectivityManager) { - this.connectivityManager = connectivityManager; - } - - String getNetworkType() { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Network network = connectivityManager.getActiveNetwork(); - NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); - if (capabilities == null) { - return "none"; - } - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) - || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { - return "wifi"; - } - if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - return "mobile"; - } - } - - return getNetworkTypeLegacy(); - } - - @SuppressWarnings("deprecation") - private String getNetworkTypeLegacy() { - // handle type for Android versions less than Android 9 - android.net.NetworkInfo info = connectivityManager.getActiveNetworkInfo(); - if (info == null || !info.isConnected()) { - return "none"; - } - int type = info.getType(); - switch (type) { - case ConnectivityManager.TYPE_ETHERNET: - case ConnectivityManager.TYPE_WIFI: - case ConnectivityManager.TYPE_WIMAX: - return "wifi"; - case ConnectivityManager.TYPE_MOBILE: - case ConnectivityManager.TYPE_MOBILE_DUN: - case ConnectivityManager.TYPE_MOBILE_HIPRI: - return "mobile"; - default: - return "none"; - } - } - - public ConnectivityManager getConnectivityManager() { - return connectivityManager; - } -} diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java deleted file mode 100644 index fbda187bd188..000000000000 --- a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java +++ /dev/null @@ -1,95 +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.connectivity; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.ConnectivityManager; -import android.net.Network; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import io.flutter.plugin.common.EventChannel; - -/** - * The ConnectivityBroadcastReceiver receives the connectivity updates and send them to the UIThread - * through an {@link EventChannel.EventSink} - * - *

Use {@link - * io.flutter.plugin.common.EventChannel#setStreamHandler(io.flutter.plugin.common.EventChannel.StreamHandler)} - * to set up the receiver. - */ -public class ConnectivityBroadcastReceiver extends BroadcastReceiver - implements EventChannel.StreamHandler { - private Context context; - private Connectivity connectivity; - private EventChannel.EventSink events; - private Handler mainHandler = new Handler(Looper.getMainLooper()); - private ConnectivityManager.NetworkCallback networkCallback; - public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; - - public ConnectivityBroadcastReceiver(Context context, Connectivity connectivity) { - this.context = context; - this.connectivity = connectivity; - } - - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - this.events = events; - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - networkCallback = - new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(Network network) { - sendEvent(); - } - - @Override - public void onLost(Network network) { - sendEvent(); - } - }; - connectivity.getConnectivityManager().registerDefaultNetworkCallback(networkCallback); - } else { - context.registerReceiver(this, new IntentFilter(CONNECTIVITY_ACTION)); - } - } - - @Override - public void onCancel(Object arguments) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (networkCallback != null) { - connectivity.getConnectivityManager().unregisterNetworkCallback(networkCallback); - networkCallback = null; - } - } else { - context.unregisterReceiver(this); - } - } - - @Override - public void onReceive(Context context, Intent intent) { - if (events != null) { - events.success(connectivity.getNetworkType()); - } - } - - public ConnectivityManager.NetworkCallback getNetworkCallback() { - return networkCallback; - } - - private void sendEvent() { - Runnable runnable = - new Runnable() { - @Override - public void run() { - events.success(connectivity.getNetworkType()); - } - }; - mainHandler.post(runnable); - } -} diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityMethodChannelHandler.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityMethodChannelHandler.java deleted file mode 100644 index 06275498c4a9..000000000000 --- a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityMethodChannelHandler.java +++ /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. - -package io.flutter.plugins.connectivity; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -/** - * The handler receives {@link MethodCall}s from the UIThread, gets the related information from - * a @{@link Connectivity}, and then send the result back to the UIThread through the {@link - * MethodChannel.Result}. - */ -class ConnectivityMethodChannelHandler implements MethodChannel.MethodCallHandler { - - private Connectivity connectivity; - - /** - * Construct the ConnectivityMethodChannelHandler with a {@code connectivity}. The {@code - * connectivity} must not be null. - */ - ConnectivityMethodChannelHandler(Connectivity connectivity) { - assert (connectivity != null); - this.connectivity = connectivity; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "check": - result.success(connectivity.getNetworkType()); - break; - default: - result.notImplemented(); - break; - } - } -} diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityPlugin.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityPlugin.java deleted file mode 100644 index 2287a0a30b86..000000000000 --- a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityPlugin.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.connectivity; - -import android.content.Context; -import android.net.ConnectivityManager; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel; - -/** ConnectivityPlugin */ -public class ConnectivityPlugin implements FlutterPlugin { - - private MethodChannel methodChannel; - private EventChannel eventChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - - ConnectivityPlugin plugin = new ConnectivityPlugin(); - plugin.setupChannels(registrar.messenger(), registrar.context()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - setupChannels(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - teardownChannels(); - } - - private void setupChannels(BinaryMessenger messenger, Context context) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/connectivity"); - eventChannel = new EventChannel(messenger, "plugins.flutter.io/connectivity_status"); - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - - Connectivity connectivity = new Connectivity(connectivityManager); - - ConnectivityMethodChannelHandler methodChannelHandler = - new ConnectivityMethodChannelHandler(connectivity); - ConnectivityBroadcastReceiver receiver = - new ConnectivityBroadcastReceiver(context, connectivity); - - methodChannel.setMethodCallHandler(methodChannelHandler); - eventChannel.setStreamHandler(receiver); - } - - private void teardownChannels() { - methodChannel.setMethodCallHandler(null); - eventChannel.setStreamHandler(null); - methodChannel = null; - eventChannel = null; - } -} diff --git a/packages/connectivity/connectivity/connectivity_android.iml b/packages/connectivity/connectivity/connectivity_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/connectivity/connectivity/connectivity_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity/example/android.iml b/packages/connectivity/connectivity/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/connectivity/connectivity/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity/example/android/app/build.gradle b/packages/connectivity/connectivity/example/android/app/build.gradle deleted file mode 100644 index 64f3d0626bf4..000000000000 --- a/packages/connectivity/connectivity/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.connectivityexample" - minSdkVersion 16 - targetSdkVersion 29 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - testImplementation 'org.robolectric:robolectric:3.8' - testImplementation 'org.mockito:mockito-core:3.5.13' -} diff --git a/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml b/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index abce0da89989..000000000000 --- a/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java deleted file mode 100644 index b4a67622f8dc..000000000000 --- a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java +++ /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. - -package io.flutter.plugins.connectivityexample; - -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/connectivity/connectivity/example/android/app/src/test/java/io/flutter/plugins/connectivityexample/ActivityTest.java b/packages/connectivity/connectivity/example/android/app/src/test/java/io/flutter/plugins/connectivityexample/ActivityTest.java deleted file mode 100644 index 2cf03dd3c2f5..000000000000 --- a/packages/connectivity/connectivity/example/android/app/src/test/java/io/flutter/plugins/connectivityexample/ActivityTest.java +++ /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. - -package io.flutter.plugins.connectivityexample; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; - -import android.content.Context; -import android.net.ConnectivityManager; -import io.flutter.plugins.connectivity.Connectivity; -import io.flutter.plugins.connectivity.ConnectivityBroadcastReceiver; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -@RunWith(RobolectricTestRunner.class) -public class ActivityTest { - private ConnectivityManager connectivityManager; - - @Before - public void setUp() { - connectivityManager = - (ConnectivityManager) - RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE); - } - - @Test - @Config(sdk = 24, manifest = Config.NONE) - public void networkCallbackNewApi() { - Context context = RuntimeEnvironment.application; - Connectivity connectivity = spy(new Connectivity(connectivityManager)); - ConnectivityBroadcastReceiver broadcastReceiver = - spy(new ConnectivityBroadcastReceiver(context, connectivity)); - - broadcastReceiver.onListen(any(), any()); - assertNotNull(broadcastReceiver.getNetworkCallback()); - } - - @Test - @Config(sdk = 23, manifest = Config.NONE) - public void networkCallbackLowApi() { - Context context = RuntimeEnvironment.application; - Connectivity connectivity = spy(new Connectivity(connectivityManager)); - ConnectivityBroadcastReceiver broadcastReceiver = - spy(new ConnectivityBroadcastReceiver(context, connectivity)); - - broadcastReceiver.onListen(any(), any()); - assertNull(broadcastReceiver.getNetworkCallback()); - } -} diff --git a/packages/connectivity/connectivity/example/android/settings.gradle b/packages/connectivity/connectivity/example/android/settings.gradle deleted file mode 100644 index a159ea7cb99f..000000000000 --- a/packages/connectivity/connectivity/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -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/connectivity/connectivity/example/connectivity_example.iml b/packages/connectivity/connectivity/example/connectivity_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/connectivity/connectivity/example/connectivity_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/connectivity/connectivity/example/connectivity_example_android.iml b/packages/connectivity/connectivity/example/connectivity_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/connectivity/connectivity/example/connectivity_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity/example/integration_test/connectivity_test.dart b/packages/connectivity/connectivity/example/integration_test/connectivity_test.dart deleted file mode 100644 index ab6e71e23bb6..000000000000 --- a/packages/connectivity/connectivity/example/integration_test/connectivity_test.dart +++ /dev/null @@ -1,24 +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:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity/connectivity.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Connectivity test driver', () { - late Connectivity _connectivity; - - setUpAll(() async { - _connectivity = Connectivity(); - }); - - testWidgets('test connectivity result', (WidgetTester tester) async { - final ConnectivityResult result = await _connectivity.checkConnectivity(); - expect(result, isNotNull); - }); - }); -} diff --git a/packages/connectivity/connectivity/example/ios/Flutter/AppFrameworkInfo.plist b/packages/connectivity/connectivity/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/connectivity/connectivity/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/connectivity/connectivity/example/ios/Podfile b/packages/connectivity/connectivity/example/ios/Podfile deleted file mode 100644 index 07a4e08abf54..000000000000 --- a/packages/connectivity/connectivity/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# 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__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - # Work around https://github.com/flutter/flutter/issues/82964. - if target.name == 'Reachability' - target.build_configurations.each do |config| - config.build_settings['WARNING_CFLAGS'] = '-Wno-pointer-to-int-cast' - end - end - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj b/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index b653a1f3b889..000000000000 --- a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,460 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C80D49AFD183103034E444C2 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3173C764DD180BE02EB51E47 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 69D903F0A9A7C636EE803AF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C80D49AFD183103034E444C2 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 89F516DEFCBF79E39D2885C2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C80D49AFD183103034E444C2 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 8ECC1C323F60D5498EEC2315 /* Pods */ = { - isa = PBXGroup; - children = ( - 69D903F0A9A7C636EE803AF8 /* Pods-Runner.debug.xcconfig */, - 3173C764DD180BE02EB51E47 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 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 */, - 8ECC1C323F60D5498EEC2315 /* Pods */, - 89F516DEFCBF79E39D2885C2 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 8122b0a0c2f2..000000000000 --- a/packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "images" : [ - { - "filename" : "Icon-App-20x20@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "Icon-App-20x20@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "Icon-App-29x29@1x.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "Icon-App-29x29@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Icon-App-29x29@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "Icon-App-40x40@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "Icon-App-40x40@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "Icon-App-60x60@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "Icon-App-60x60@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "Icon-App-20x20@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "Icon-App-20x20@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "Icon-App-29x29@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "Icon-App-29x29@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "Icon-App-40x40@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "Icon-App-40x40@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "Icon-App-76x76@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "filename" : "Icon-App-76x76@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "Icon-App-83.5x83.5@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/packages/connectivity/connectivity/example/ios/Runner/Info.plist b/packages/connectivity/connectivity/example/ios/Runner/Info.plist deleted file mode 100644 index d76382b40acf..000000000000 --- a/packages/connectivity/connectivity/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - connectivity_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/connectivity/connectivity/example/ios/Runner/Runner.entitlements b/packages/connectivity/connectivity/example/ios/Runner/Runner.entitlements deleted file mode 100644 index ba21fbdaf290..000000000000 --- a/packages/connectivity/connectivity/example/ios/Runner/Runner.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.developer.networking.wifi-info - - - diff --git a/packages/connectivity/connectivity/example/ios/Runner/main.m b/packages/connectivity/connectivity/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/connectivity/connectivity/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/connectivity/connectivity/example/lib/main.dart b/packages/connectivity/connectivity/example/lib/main.dart deleted file mode 100644 index b6a6882cb12e..000000000000 --- a/packages/connectivity/connectivity/example/lib/main.dart +++ /dev/null @@ -1,112 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:connectivity/connectivity.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -// Sets a platform override for desktop to avoid exceptions. See -// https://flutter.dev/desktop#target-platform-override for more info. -void _enablePlatformOverrideForDesktop() { - if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { - debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - } -} - -void main() { - _enablePlatformOverrideForDesktop(); - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String _connectionStatus = 'Unknown'; - final Connectivity _connectivity = Connectivity(); - late StreamSubscription _connectivitySubscription; - - @override - void initState() { - super.initState(); - initConnectivity(); - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); - } - - @override - void dispose() { - _connectivitySubscription.cancel(); - super.dispose(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initConnectivity() async { - ConnectivityResult result = ConnectivityResult.none; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - result = await _connectivity.checkConnectivity(); - } on PlatformException catch (e) { - print(e.toString()); - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) { - return Future.value(null); - } - - return _updateConnectionStatus(result); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Connectivity example app'), - ), - body: Center(child: Text('Connection Status: $_connectionStatus')), - ); - } - - Future _updateConnectionStatus(ConnectivityResult result) async { - switch (result) { - case ConnectivityResult.wifi: - case ConnectivityResult.mobile: - case ConnectivityResult.none: - setState(() => _connectionStatus = result.toString()); - break; - default: - setState(() => _connectionStatus = 'Failed to get connectivity.'); - break; - } - } -} diff --git a/packages/connectivity/connectivity/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/connectivity/connectivity/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/connectivity/connectivity/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/connectivity/connectivity/example/macos/Flutter/Flutter-Release.xcconfig b/packages/connectivity/connectivity/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/connectivity/connectivity/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/connectivity/connectivity/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/connectivity/connectivity/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 999432406891..000000000000 --- a/packages/connectivity/connectivity/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import connectivity_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) -} diff --git a/packages/connectivity/connectivity/example/macos/Runner.xcodeproj/project.pbxproj b/packages/connectivity/connectivity/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 0e2413493f6e..000000000000 --- a/packages/connectivity/connectivity/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,654 +0,0 @@ -// !$*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 */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 748ADDF1719804343BB18004 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 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 = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* connectivity_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = connectivity_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 = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 748ADDF1719804343BB18004 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - EA473EC5F2038B17A2FE4D78 /* 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 */, - D42EAEE5849744148CC78D83 /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* connectivity_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 */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, - ); - 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 = ""; - }; - D42EAEE5849744148CC78D83 /* Pods */ = { - isa = PBXGroup; - children = ( - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */, - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */, - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 748ADDF1719804343BB18004 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* connectivity_example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "Google LLC"; - 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 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\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\ntouch Flutter/ephemeral/tripwire\n"; - }; - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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/connectivity/connectivity/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/connectivity/connectivity/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 2a7d3e7f34ac..000000000000 --- a/packages/connectivity/connectivity/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 1a9e76c10a78..000000000000 --- a/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// 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 = connectivity_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors.flutter.plugins. All rights reserved. diff --git a/packages/connectivity/connectivity/example/macos/Runner/DebugProfile.entitlements b/packages/connectivity/connectivity/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/connectivity/connectivity/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/packages/connectivity/connectivity/example/macos/Runner/Release.entitlements b/packages/connectivity/connectivity/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/connectivity/connectivity/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/connectivity/connectivity/example/pubspec.yaml b/packages/connectivity/connectivity/example/pubspec.yaml deleted file mode 100644 index 1707d3482a98..000000000000 --- a/packages/connectivity/connectivity/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: connectivity_example -description: Demonstrates how to use the connectivity plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - connectivity: - # When depending on this package from a real application you should use: - # connectivity: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - test: ^1.16.3 - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/connectivity/connectivity/example/web/index.html b/packages/connectivity/connectivity/example/web/index.html deleted file mode 100644 index c6fa1623be95..000000000000 --- a/packages/connectivity/connectivity/example/web/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - example - - - - - - - - diff --git a/packages/connectivity/connectivity/example/web/manifest.json b/packages/connectivity/connectivity/example/web/manifest.json deleted file mode 100644 index 8c012917dab7..000000000000 --- a/packages/connectivity/connectivity/example/web/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "example", - "short_name": "example", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} diff --git a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h deleted file mode 100644 index aec76adc0b58..000000000000 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTConnectivityPlugin : NSObject -@end diff --git a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m deleted file mode 100644 index ac37c2359137..000000000000 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m +++ /dev/null @@ -1,92 +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 "FLTConnectivityPlugin.h" - -#import "Reachability/Reachability.h" - -#import -#import "SystemConfiguration/CaptiveNetwork.h" - -#include - -#include - -@interface FLTConnectivityPlugin () - -@end - -@implementation FLTConnectivityPlugin { - FlutterEventSink _eventSink; - Reachability* _reachabilityForInternetConnection; -} - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTConnectivityPlugin* instance = [[FLTConnectivityPlugin alloc] init]; - - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/connectivity" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; - - FlutterEventChannel* streamChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/connectivity_status" - binaryMessenger:[registrar messenger]]; - [streamChannel setStreamHandler:instance]; -} - -- (NSString*)statusFromReachability:(Reachability*)reachability { - NetworkStatus status = [reachability currentReachabilityStatus]; - switch (status) { - case NotReachable: - return @"none"; - case ReachableViaWiFi: - return @"wifi"; - case ReachableViaWWAN: - return @"mobile"; - } -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"check"]) { - // This is supposed to be quick. Another way of doing this would be to - // signup for network - // connectivity changes. However that depends on the app being in background - // and the code - // gets more involved. So for now, this will do. - result([self statusFromReachability:[Reachability reachabilityForInternetConnection]]); - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)onReachabilityDidChange:(NSNotification*)notification { - Reachability* curReach = [notification object]; - _eventSink([self statusFromReachability:curReach]); -} - -#pragma mark FlutterStreamHandler impl - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _eventSink = eventSink; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onReachabilityDidChange:) - name:kReachabilityChangedNotification - object:nil]; - _reachabilityForInternetConnection = [Reachability reachabilityForInternetConnection]; - [_reachabilityForInternetConnection startNotifier]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - if (_reachabilityForInternetConnection) { - [_reachabilityForInternetConnection stopNotifier]; - _reachabilityForInternetConnection = nil; - } - [[NSNotificationCenter defaultCenter] removeObserver:self]; - _eventSink = nil; - return nil; -} - -@end diff --git a/packages/connectivity/connectivity/ios/connectivity.podspec b/packages/connectivity/connectivity/ios/connectivity.podspec deleted file mode 100644 index 096a76b8d3c3..000000000000 --- a/packages/connectivity/connectivity/ios/connectivity.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'connectivity' - s.version = '0.0.1' - s.summary = 'Flutter Connectivity' - s.description = <<-DESC -This plugin allows Flutter apps to discover network connectivity and configure themselves accordingly. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity' } - s.documentation_url = 'https://pub.dev/packages/connectivity' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.dependency 'Reachability' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/connectivity/connectivity/lib/connectivity.dart b/packages/connectivity/connectivity/lib/connectivity.dart deleted file mode 100644 index 1b819d7470d2..000000000000 --- a/packages/connectivity/connectivity/lib/connectivity.dart +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -// Export enums from the platform_interface so plugin users can use them directly. -export 'package:connectivity_platform_interface/connectivity_platform_interface.dart' - show ConnectivityResult, LocationAuthorizationStatus; - -/// Discover network connectivity configurations: Distinguish between WI-FI and cellular, check WI-FI status and more. -class Connectivity { - /// Constructs a singleton instance of [Connectivity]. - /// - /// [Connectivity] is designed to work as a singleton. - // When a second instance is created, the first instance will not be able to listen to the - // EventChannel because it is overridden. Forcing the class to be a singleton class can prevent - // misuse of creating a second instance from a programmer. - factory Connectivity() { - if (_singleton == null) { - _singleton = Connectivity._(); - } - return _singleton!; - } - - Connectivity._(); - - static Connectivity? _singleton; - - static ConnectivityPlatform get _platform => ConnectivityPlatform.instance; - - /// Fires whenever the connectivity state changes. - Stream get onConnectivityChanged { - return _platform.onConnectivityChanged; - } - - /// Checks the connection status of the device. - /// - /// Do not use the result of this function to decide whether you can reliably - /// make a network request. It only gives you the radio status. - /// - /// Instead listen for connectivity changes via [onConnectivityChanged] stream. - Future checkConnectivity() { - return _platform.checkConnectivity(); - } -} diff --git a/packages/connectivity/connectivity/pubspec.yaml b/packages/connectivity/connectivity/pubspec.yaml deleted file mode 100644 index 16e179cfa085..000000000000 --- a/packages/connectivity/connectivity/pubspec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: connectivity -description: Flutter plugin for discovering the state of the network (WiFi & - mobile/cellular) connectivity on Android and iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 3.0.6 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.connectivity - pluginClass: ConnectivityPlugin - ios: - pluginClass: FLTConnectivityPlugin - macos: - default_package: connectivity_macos - web: - default_package: connectivity_for_web - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - connectivity_platform_interface: ^2.0.0 - connectivity_macos: ^0.2.0 - connectivity_for_web: ^0.4.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - plugin_platform_interface: ^2.0.0 - pedantic: ^1.10.0 - test: ^1.16.3 diff --git a/packages/connectivity/connectivity/test/connectivity_test.dart b/packages/connectivity/connectivity/test/connectivity_test.dart deleted file mode 100644 index e6c253e0d49a..000000000000 --- a/packages/connectivity/connectivity/test/connectivity_test.dart +++ /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 'package:connectivity/connectivity.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/fake.dart'; - -const ConnectivityResult kCheckConnectivityResult = ConnectivityResult.wifi; -const LocationAuthorizationStatus kRequestLocationResult = - LocationAuthorizationStatus.authorizedAlways; -const LocationAuthorizationStatus kGetLocationResult = - LocationAuthorizationStatus.authorizedAlways; - -void main() { - group('Connectivity', () { - late Connectivity connectivity; - late MockConnectivityPlatform fakePlatform; - setUp(() async { - fakePlatform = MockConnectivityPlatform(); - ConnectivityPlatform.instance = fakePlatform; - connectivity = Connectivity(); - }); - - test('checkConnectivity', () async { - ConnectivityResult result = await connectivity.checkConnectivity(); - expect(result, kCheckConnectivityResult); - }); - }); -} - -class MockConnectivityPlatform extends Fake - with MockPlatformInterfaceMixin - implements ConnectivityPlatform { - Future checkConnectivity() async { - return kCheckConnectivityResult; - } -} diff --git a/packages/connectivity/connectivity_for_web/.gitignore b/packages/connectivity/connectivity_for_web/.gitignore deleted file mode 100644 index d7dee828a6b9..000000000000 --- a/packages/connectivity/connectivity_for_web/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -.dart_tool/ - -.packages -.pub/ - -build/ -lib/generated_plugin_registrant.dart diff --git a/packages/connectivity/connectivity_for_web/.metadata b/packages/connectivity/connectivity_for_web/.metadata deleted file mode 100644 index 23eb55ba6da2..000000000000 --- a/packages/connectivity/connectivity_for_web/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: 52ee8a6c6565cd421dfa32042941eb40691f4746 - channel: master - -project_type: plugin diff --git a/packages/connectivity/connectivity_for_web/CHANGELOG.md b/packages/connectivity/connectivity_for_web/CHANGELOG.md deleted file mode 100644 index 97e5032c8dd4..000000000000 --- a/packages/connectivity/connectivity_for_web/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -## 0.4.0+1 - -* Add `implements` to pubspec. - -## 0.4.0 - -* Migrate to null-safety -* Run tests using flutter driver - -## 0.3.1+4 - -* Remove unused `test` dependency. - -## 0.3.1+3 - -* Fix homepage in `pubspec.yaml`. - -## 0.3.1+2 - -* Update package:e2e to use package:integration_test - -## 0.3.1+1 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.3.1 - -* Use NetworkInformation API from dart:html, instead of the JS-interop version. - -## 0.3.0 - -* Rename from "experimental_connectivity_web" to "connectivity_for_web", and move to flutter/plugins master. - -## 0.2.0 - -* Add fallback on dart:html for browsers where NetworkInformationAPI is not supported. - -## 0.1.0 - -* Initial release. diff --git a/packages/connectivity/connectivity_for_web/README.md b/packages/connectivity/connectivity_for_web/README.md deleted file mode 100644 index 66efc49fc840..000000000000 --- a/packages/connectivity/connectivity_for_web/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# connectivity_for_web - -A web implementation of [connectivity](https://pub.dev/connectivity/connectivity). Currently this package uses an experimental API, with a fallback to dart:html, so not all features may be available to all browsers. - -## Usage - -### Import the package - -This package is a non-endorsed implementation of `connectivity` for the web platform, so you need to modify your `pubspec.yaml` to use it: - -```yaml -... -dependencies: - ... - connectivity: ^0.4.9 - connectivity_for_web: ^0.3.0 - ... -... -``` - -## Example - -Find the example wiring in the [Google sign-in example application](https://github.com/ditman/plugins/blob/connectivity-web/packages/connectivity/connectivity/example/lib/main.dart). - -## Limitations on the web platform - -In order to retrieve information about the quality/speed of a browser's connection, the web implementation of the `connectivity` plugin uses the browser's [**NetworkInformation** Web API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation), which as of this writing (June 2020) is still "experimental", and not available in all browsers: - -![Data on support for the netinfo feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/netinfo.png) - -On desktop browsers, this API only returns a very broad set of connectivity statuses (One of `'slow-2g', '2g', '3g', or '4g'`), and may *not* provide a Stream of changes. Firefox still hasn't enabled this feature by default. - -**Fallback to `navigator.onLine`** - -For those browsers where the NetworkInformation Web API is not available, the plugin falls back to the [**NavigatorOnLine** Web API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine), which is more broadly supported: - -![Data on support for the online-status feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/online-status.png) - - -The NavigatorOnLine API is [provided by `dart:html`](https://api.dart.dev/stable/2.7.2/dart-html/Navigator/onLine.html), and only supports a boolean connectivity status (either online or offline), with no network speed information. In those cases the plugin will return either `wifi` (when the browser is online) or `none` (when it's not). - -Other than the approximate "downlink" speed, where available, and due to security and privacy concerns, **no Web browser will provide** any specific information about the actual network your users' device is connected to, like **the SSID on a Wi-Fi, or the MAC address of their device.** - -## Contributions and Testing - -Tests are crucial to contributions to this package. All new contributions should be reasonably tested. - -In order to run tests in this package, do: - -``` -cd test -flutter run -d chrome -``` - -All 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. - -## Issues and feedback - -Please file an [issue](https://github.com/ditman/plugins/issues/new) -to send feedback or report a bug. - -**Thank you!** diff --git a/packages/connectivity/connectivity_for_web/example/README.md b/packages/connectivity/connectivity_for_web/example/README.md deleted file mode 100644 index 8a6e74b107ea..000000000000 --- a/packages/connectivity/connectivity_for_web/example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Testing - -This package uses `package:integration_test` to run its tests in a web browser. - -See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) -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 diff --git a/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart b/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart deleted file mode 100644 index e6faa30da4dc..000000000000 --- a/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart +++ /dev/null @@ -1,83 +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:connectivity_for_web/src/network_information_api_connectivity_plugin.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'src/connectivity_mocks.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('checkConnectivity', () { - void testCheckConnectivity({ - String? type, - String? effectiveType, - num? downlink = 10, - int? rtt = 50, - required ConnectivityResult expected, - }) { - final connection = FakeNetworkInformation( - type: type, - effectiveType: effectiveType, - downlink: downlink, - rtt: rtt, - ); - - NetworkInformationApiConnectivityPlugin plugin = - NetworkInformationApiConnectivityPlugin.withConnection(connection); - expect(plugin.checkConnectivity(), completion(equals(expected))); - } - - testWidgets('0 downlink and rtt -> none', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '4g', - downlink: 0, - rtt: 0, - expected: ConnectivityResult.none); - }); - testWidgets('slow-2g -> mobile', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: 'slow-2g', expected: ConnectivityResult.mobile); - }); - testWidgets('2g -> mobile', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '2g', expected: ConnectivityResult.mobile); - }); - testWidgets('3g -> mobile', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '3g', expected: ConnectivityResult.mobile); - }); - testWidgets('4g -> wifi', (WidgetTester tester) async { - testCheckConnectivity( - effectiveType: '4g', expected: ConnectivityResult.wifi); - }); - }); - - group('get onConnectivityChanged', () { - testWidgets('puts change events in a Stream', (WidgetTester tester) async { - final connection = FakeNetworkInformation(); - NetworkInformationApiConnectivityPlugin plugin = - NetworkInformationApiConnectivityPlugin.withConnection(connection); - - // The onConnectivityChanged stream is infinite, so we only .take(2) so the test completes. - // We need to do .toList() now, because otherwise the Stream won't be actually listened to, - // and we'll miss the calls to mockChangeValue below. - final results = plugin.onConnectivityChanged.take(2).toList(); - - // Fake a disconnect-reconnect - await connection.mockChangeValue(downlink: 0, rtt: 0); - await connection.mockChangeValue( - downlink: 10, rtt: 50, effectiveType: '4g'); - - // Expect to see the disconnect-reconnect in the resulting stream. - expect( - results, - completion([ConnectivityResult.none, ConnectivityResult.wifi]), - ); - }); - }); -} diff --git a/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.dart b/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.dart deleted file mode 100644 index 556b6fe6fca0..000000000000 --- a/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.dart +++ /dev/null @@ -1,60 +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:html'; -import 'dart:js_util' show getProperty; - -import 'package:flutter_test/flutter_test.dart'; - -/// A Fake implementation of the NetworkInformation API that allows -/// for external modification of its values. -/// -/// Note that the DOM API works by internally mutating and broadcasting -/// 'change' events. -class FakeNetworkInformation extends Fake implements NetworkInformation { - String? _type; - String? _effectiveType; - num? _downlink; - int? _rtt; - - @override - String? get type => _type; - - @override - String? get effectiveType => _effectiveType; - - @override - num? get downlink => _downlink; - - @override - int? get rtt => _rtt; - - FakeNetworkInformation({ - String? type, - String? effectiveType, - num? downlink, - int? rtt, - }) : this._type = type, - this._effectiveType = effectiveType, - this._downlink = downlink, - this._rtt = rtt; - - /// Changes the desired values, and triggers the change event listener. - Future mockChangeValue({ - String? type, - String? effectiveType, - num? downlink, - int? rtt, - }) async { - this._type = type; - this._effectiveType = effectiveType; - this._downlink = downlink; - this._rtt = rtt; - - // This is set by the onConnectivityChanged getter... - final Function onchange = getProperty(this, 'onchange') as Function; - onchange(Event('change')); - } -} diff --git a/packages/connectivity/connectivity_for_web/example/lib/main.dart b/packages/connectivity/connectivity_for_web/example/lib/main.dart deleted file mode 100644 index e1a38dcdcd46..000000000000 --- a/packages/connectivity/connectivity_for_web/example/lib/main.dart +++ /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 'package:flutter/material.dart'; - -void main() { - runApp(MyApp()); -} - -/// App for testing -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: TextDirection.ltr, - child: Text('Testing... Look at the console output for results!'), - ); - } -} diff --git a/packages/connectivity/connectivity_for_web/example/pubspec.yaml b/packages/connectivity/connectivity_for_web/example/pubspec.yaml deleted file mode 100644 index 3b8e209e2486..000000000000 --- a/packages/connectivity/connectivity_for_web/example/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: connectivity_for_web_integration_tests -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" - -dependencies: - connectivity_for_web: - path: ../ - flutter: - sdk: flutter - -dev_dependencies: - js: ^0.6.3 - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter diff --git a/packages/connectivity/connectivity_for_web/example/run_test.sh b/packages/connectivity/connectivity_for_web/example/run_test.sh deleted file mode 100755 index aa52974f310e..000000000000 --- a/packages/connectivity/connectivity_for_web/example/run_test.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/bash -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -if pgrep -lf chromedriver > /dev/null; then - echo "chromedriver is running." - - if [ $# -eq 0 ]; then - echo "No target specified, running all tests..." - find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' - else - echo "Running test target: $1..." - set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 - fi - - else - echo "chromedriver is not running." - echo "Please, check the README.md for instructions on how to use run_test.sh" -fi - diff --git a/packages/connectivity/connectivity_for_web/example/web/index.html b/packages/connectivity/connectivity_for_web/example/web/index.html deleted file mode 100644 index 7fb138cc90fa..000000000000 --- a/packages/connectivity/connectivity_for_web/example/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - example - - - - - diff --git a/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart b/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart deleted file mode 100644 index d1c6811f5349..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -import 'src/network_information_api_connectivity_plugin.dart'; -import 'src/dart_html_connectivity_plugin.dart'; - -/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. -class ConnectivityPlugin extends ConnectivityPlatform { - /// Factory method that initializes the connectivity plugin platform with an instance - /// of the plugin for the web. - static void registerWith(Registrar registrar) { - if (NetworkInformationApiConnectivityPlugin.isSupported()) { - ConnectivityPlatform.instance = NetworkInformationApiConnectivityPlugin(); - } else { - ConnectivityPlatform.instance = DartHtmlConnectivityPlugin(); - } - } - - // The following are completely unsupported methods on the web platform. - - // Creates an "unsupported_operation" PlatformException for a given `method` name. - Object _unsupported(String method) { - return PlatformException( - code: 'UNSUPPORTED_OPERATION', - message: '$method() is not supported on the web platform.', - ); - } - - /// Obtains the wifi name (SSID) of the connected network - @override - Future getWifiName() { - throw _unsupported('getWifiName'); - } - - /// Obtains the wifi BSSID of the connected network. - @override - Future getWifiBSSID() { - throw _unsupported('getWifiBSSID'); - } - - /// Obtains the IP address of the connected wifi network - @override - Future getWifiIP() { - throw _unsupported('getWifiIP'); - } - - /// Request to authorize the location service (Only on iOS). - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) { - throw _unsupported('requestLocationServiceAuthorization'); - } - - /// Get the current location service authorization (Only on iOS). - @override - Future getLocationServiceAuthorization() { - throw _unsupported('getLocationServiceAuthorization'); - } -} diff --git a/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart b/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart deleted file mode 100644 index 475ec0d675b7..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart +++ /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 'dart:async'; -import 'dart:html' as html show window; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:connectivity_for_web/connectivity_for_web.dart'; - -/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. -class DartHtmlConnectivityPlugin extends ConnectivityPlugin { - /// Checks the connection status of the device. - @override - Future checkConnectivity() async { - return html.window.navigator.onLine ?? false - ? ConnectivityResult.wifi - : ConnectivityResult.none; - } - - StreamController? _connectivityResult; - - /// Returns a Stream of ConnectivityResults changes. - @override - Stream get onConnectivityChanged { - if (_connectivityResult == null) { - _connectivityResult = StreamController.broadcast(); - // Fallback to dart:html window.onOnline / window.onOffline - html.window.onOnline.listen((event) { - _connectivityResult!.add(ConnectivityResult.wifi); - }); - html.window.onOffline.listen((event) { - _connectivityResult!.add(ConnectivityResult.none); - }); - } - return _connectivityResult!.stream; - } -} diff --git a/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart b/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart deleted file mode 100644 index 6554f7a8c124..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:html' as html show window, NetworkInformation; -import 'dart:js' show allowInterop; -import 'dart:js_util' show setProperty; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:connectivity_for_web/connectivity_for_web.dart'; -import 'package:flutter/foundation.dart'; - -import 'utils/connectivity_result.dart'; - -/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. -class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { - final html.NetworkInformation _networkInformation; - - /// A check to determine if this version of the plugin can be used. - static bool isSupported() => html.window.navigator.connection != null; - - /// The constructor of the plugin. - NetworkInformationApiConnectivityPlugin() - : this.withConnection(html.window.navigator.connection!); - - /// Creates the plugin, with an override of the NetworkInformation object. - @visibleForTesting - NetworkInformationApiConnectivityPlugin.withConnection( - html.NetworkInformation connection) - : _networkInformation = connection; - - /// Checks the connection status of the device. - @override - Future checkConnectivity() async { - return networkInformationToConnectivityResult(_networkInformation); - } - - StreamController? _connectivityResultStreamController; - late Stream _connectivityResultStream; - - /// Returns a Stream of ConnectivityResults changes. - @override - Stream get onConnectivityChanged { - if (_connectivityResultStreamController == null) { - _connectivityResultStreamController = - StreamController(); - - // Directly write the 'onchange' function on the networkInformation object. - setProperty(_networkInformation, 'onchange', allowInterop((_) { - _connectivityResultStreamController! - .add(networkInformationToConnectivityResult(_networkInformation)); - })); - // TODO: Implement the above with _networkInformation.onChange: - // _networkInformation.onChange.listen((_) { - // _connectivityResult - // .add(networkInformationToConnectivityResult(_networkInformation)); - // }); - // Once we can detect when to *cancel* a subscription to the _networkInformation - // onChange Stream upon hot restart. - // https://github.com/dart-lang/sdk/issues/42679 - _connectivityResultStream = - _connectivityResultStreamController!.stream.asBroadcastStream(); - } - return _connectivityResultStream; - } -} diff --git a/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart b/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart deleted file mode 100644 index 691bd6da3bfb..000000000000 --- a/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html show NetworkInformation; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -/// Converts an incoming NetworkInformation object into the correct ConnectivityResult. -ConnectivityResult networkInformationToConnectivityResult( - html.NetworkInformation? info, -) { - if (info == null) { - return ConnectivityResult.none; - } - if (info.downlink == 0 && info.rtt == 0) { - return ConnectivityResult.none; - } - if (info.effectiveType != null) { - return _effectiveTypeToConnectivityResult(info.effectiveType!); - } - if (info.type != null) { - return _typeToConnectivityResult(info.type!); - } - return ConnectivityResult.none; -} - -ConnectivityResult _effectiveTypeToConnectivityResult(String effectiveType) { - // Possible values: - /*'2g'|'3g'|'4g'|'slow-2g'*/ - switch (effectiveType) { - case 'slow-2g': - case '2g': - case '3g': - return ConnectivityResult.mobile; - default: - return ConnectivityResult.wifi; - } -} - -ConnectivityResult _typeToConnectivityResult(String type) { - // Possible values: - /*'bluetooth'|'cellular'|'ethernet'|'mixed'|'none'|'other'|'unknown'|'wifi'|'wimax'*/ - switch (type) { - case 'none': - return ConnectivityResult.none; - case 'bluetooth': - case 'cellular': - case 'mixed': - case 'other': - case 'unknown': - return ConnectivityResult.mobile; - default: - return ConnectivityResult.wifi; - } -} diff --git a/packages/connectivity/connectivity_for_web/pubspec.yaml b/packages/connectivity/connectivity_for_web/pubspec.yaml deleted file mode 100644 index 2aaa8bd978fa..000000000000 --- a/packages/connectivity/connectivity_for_web/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: connectivity_for_web -description: An implementation for the web platform of the Flutter `connectivity` plugin. This uses the NetworkInformation Web API, with a fallback to Navigator.onLine. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.4.0+1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - implements: connectivity - platforms: - web: - pluginClass: ConnectivityPlugin - fileName: connectivity_for_web.dart - -dependencies: - connectivity_platform_interface: ^2.0.0 - flutter_web_plugins: - sdk: flutter - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter diff --git a/packages/connectivity/connectivity_for_web/test/README.md b/packages/connectivity/connectivity_for_web/test/README.md deleted file mode 100644 index 7c5b4ad682ba..000000000000 --- a/packages/connectivity/connectivity_for_web/test/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## test - -This package uses integration tests for testing. - -See `example/README.md` for more info. diff --git a/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart b/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart deleted file mode 100644 index 442c50144727..000000000000 --- a/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('Tell the user where to find the real tests', () { - print('---'); - print('This package uses integration_test for its tests.'); - print('See `example/README.md` for more info.'); - print('---'); - }); -} diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md deleted file mode 100644 index 46a4038f91ee..000000000000 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ /dev/null @@ -1,69 +0,0 @@ -## 0.2.1+2 - -* Add Swift language version to podspec. -* Fix `implements` package name in pubspec. - -## 0.2.1+1 - -* Ignore Reachability pointer to int cast warning. - -## 0.2.1 - -* Add `implements` to pubspec.yaml. - -## 0.2.0 - -* Remove placeholder Dart file. -* Update Dart SDK constraint for compatibility with null safety. - -## 0.1.0+8 - -* Update Flutter SDK constraint. - -## 0.1.0+7 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 0.1.0+6 - -* Update license headers. - -## 0.1.0+5 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. -* Remove no-op android folder in the example app. - -## 0.1.0+4 - -* Remove Android folder from `connectivity_macos`. - -## 0.1.0+3 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Clean up various Android workarounds no longer needed after framework v1.12. -* Complete v2 embedding support. -* Fix CocoaPods podspec lint warnings. -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.1.0+2 - -* Remove hard coded ios workspace setting of the example app. - -## 0.1.0+1 - -* Make the pedantic dev_dependency explicit. - -## 0.1.0 - -* Adds an example app to trigger CI tests. Bumped the MINOR version to -avoid compatibility issues once this packages is endorsed. - -## 0.0.2+1 - -* Add CHANGELOG. - -## 0.0.1 - -* Initial open source release. diff --git a/packages/connectivity/connectivity_macos/README.md b/packages/connectivity/connectivity_macos/README.md deleted file mode 100644 index 6974fd1fcc7e..000000000000 --- a/packages/connectivity/connectivity_macos/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# connectivity_macos - -The macos implementation of [`connectivity`]. - -## Usage - -### Import the package - - -This package has been endorsed, meaning that you only need to add `connectivity` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:connectivity`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - connectivity: ^0.4.8 - ... -``` - -Refer to the `connectivity` [documentation](https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity) for more details. diff --git a/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart b/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart deleted file mode 100644 index 3e2b1c008a84..000000000000 --- a/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart +++ /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 'dart:io'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Connectivity test driver', () { - late ConnectivityPlatform _connectivity; - - setUpAll(() async { - _connectivity = ConnectivityPlatform.instance; - }); - - testWidgets('test connectivity result', (WidgetTester tester) async { - final ConnectivityResult result = await _connectivity.checkConnectivity(); - expect(result, isNotNull); - switch (result) { - case ConnectivityResult.wifi: - expect(_connectivity.getWifiName(), completes); - expect(_connectivity.getWifiBSSID(), completes); - expect((await _connectivity.getWifiIP()), isNotNull); - break; - default: - break; - } - }); - - testWidgets('test location methods, iOS only', (WidgetTester tester) async { - if (Platform.isIOS) { - expect((await _connectivity.getLocationServiceAuthorization()), - LocationAuthorizationStatus.notDetermined); - } - }); - }); -} diff --git a/packages/connectivity/connectivity_macos/example/lib/main.dart b/packages/connectivity/connectivity_macos/example/lib/main.dart deleted file mode 100644 index d0e07341fe92..000000000000 --- a/packages/connectivity/connectivity_macos/example/lib/main.dart +++ /dev/null @@ -1,172 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -// Sets a platform override for desktop to avoid exceptions. See -// https://flutter.dev/desktop#target-platform-override for more info. -void _enablePlatformOverrideForDesktop() { - if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { - debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - } -} - -void main() { - _enablePlatformOverrideForDesktop(); - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, this.title}) : super(key: key); - - final String? title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String _connectionStatus = 'Unknown'; - final ConnectivityPlatform _connectivity = ConnectivityPlatform.instance; - late StreamSubscription _connectivitySubscription; - - @override - void initState() { - super.initState(); - initConnectivity(); - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); - } - - @override - void dispose() { - _connectivitySubscription.cancel(); - super.dispose(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initConnectivity() async { - late ConnectivityResult result; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - result = await _connectivity.checkConnectivity(); - } on PlatformException catch (e) { - print(e.toString()); - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) { - return Future.value(null); - } - - return _updateConnectionStatus(result); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center(child: Text('Connection Status: $_connectionStatus')), - ); - } - - Future _updateConnectionStatus(ConnectivityResult result) async { - switch (result) { - case ConnectivityResult.wifi: - String? wifiName, wifiBSSID, wifiIP; - - try { - if (Platform.isIOS) { - LocationAuthorizationStatus status = - await _connectivity.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = - await _connectivity.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiName = await _connectivity.getWifiName(); - } else { - wifiName = await _connectivity.getWifiName(); - } - } else { - wifiName = await _connectivity.getWifiName(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiName = "Failed to get Wifi Name"; - } - - try { - if (Platform.isIOS) { - LocationAuthorizationStatus status = - await _connectivity.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = - await _connectivity.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiBSSID = await _connectivity.getWifiBSSID(); - } else { - wifiBSSID = await _connectivity.getWifiBSSID(); - } - } else { - wifiBSSID = await _connectivity.getWifiBSSID(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiBSSID = "Failed to get Wifi BSSID"; - } - - try { - wifiIP = await _connectivity.getWifiIP(); - } on PlatformException catch (e) { - print(e.toString()); - wifiIP = "Failed to get Wifi IP"; - } - - setState(() { - _connectionStatus = '$result\n' - 'Wifi Name: $wifiName\n' - 'Wifi BSSID: $wifiBSSID\n' - 'Wifi IP: $wifiIP\n'; - }); - break; - case ConnectivityResult.mobile: - case ConnectivityResult.none: - setState(() => _connectionStatus = result.toString()); - break; - default: - setState(() => _connectionStatus = 'Failed to get connectivity.'); - break; - } - } -} diff --git a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/macos/Podfile b/packages/connectivity/connectivity_macos/example/macos/Podfile deleted file mode 100644 index e9131e75c4d2..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Podfile +++ /dev/null @@ -1,46 +0,0 @@ -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| - # Work around https://github.com/flutter/flutter/issues/82964. - if target.name == 'Reachability' - target.build_configurations.each do |config| - config.build_settings['WARNING_CFLAGS'] = '-Wno-pointer-to-int-cast' - end - end - flutter_additional_macos_build_settings(target) - end -end diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 0e2413493f6e..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,654 +0,0 @@ -// !$*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 */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 748ADDF1719804343BB18004 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 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 = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* connectivity_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = connectivity_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 = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 748ADDF1719804343BB18004 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - EA473EC5F2038B17A2FE4D78 /* 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 */, - D42EAEE5849744148CC78D83 /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* connectivity_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 */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, - ); - 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 = ""; - }; - D42EAEE5849744148CC78D83 /* Pods */ = { - isa = PBXGroup; - children = ( - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */, - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */, - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 748ADDF1719804343BB18004 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* connectivity_example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "Google LLC"; - 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 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\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\ntouch Flutter/ephemeral/tripwire\n"; - }; - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 2a7d3e7f34ac..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/connectivity/connectivity_macos/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 537341abf994..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index db9bebac4b66..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// 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 = connectivity_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/DebugProfile.entitlements b/packages/connectivity/connectivity_macos/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Release.entitlements b/packages/connectivity/connectivity_macos/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/connectivity/connectivity_macos/example/pubspec.yaml b/packages/connectivity/connectivity_macos/example/pubspec.yaml deleted file mode 100644 index 0af2e1587b00..000000000000 --- a/packages/connectivity/connectivity_macos/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: connectivity_example -description: Demonstrates how to use the connectivity plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.10.0" - -dependencies: - flutter: - sdk: flutter - connectivity_platform_interface: ^2.0.0 - connectivity_macos: - # When depending on this package from a real application you should use: - # connectivity_macos: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift b/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift deleted file mode 100644 index 69efe80df5ac..000000000000 --- a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift +++ /dev/null @@ -1,94 +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 Cocoa -import CoreWLAN -import FlutterMacOS -import Reachability -import SystemConfiguration.CaptiveNetwork - -public class ConnectivityPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { - var reach: Reachability? - var eventSink: FlutterEventSink? - var cwinterface: CWInterface? - - public override init() { - cwinterface = CWWiFiClient.shared().interface() - } - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/connectivity", - binaryMessenger: registrar.messenger) - - let streamChannel = FlutterEventChannel( - name: "plugins.flutter.io/connectivity_status", - binaryMessenger: registrar.messenger) - - let instance = ConnectivityPlugin() - streamChannel.setStreamHandler(instance) - - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "check": - result(statusFromReachability(reachability: Reachability.forInternetConnection())) - case "wifiName": - result(cwinterface?.ssid()) - case "wifiBSSID": - result(cwinterface?.bssid()) - case "wifiIPAddress": - result(getWifiIP()) - default: - result(FlutterMethodNotImplemented) - } - } - - /// Returns a string describing connection type - /// - /// - Parameters: - /// - reachability: an instance of reachability - /// - Returns: connection type string - private func statusFromReachability(reachability: Reachability?) -> String { - // checks any non-WWAN connection - if reachability?.isReachableViaWiFi() ?? false { - return "wifi" - } - - return "none" - } - - public func onListen( - withArguments _: Any?, - eventSink events: @escaping FlutterEventSink - ) -> FlutterError? { - reach = Reachability.forInternetConnection() - eventSink = events - - NotificationCenter.default.addObserver( - self, - selector: #selector(reachabilityChanged), - name: NSNotification.Name.reachabilityChanged, - object: reach) - - reach?.startNotifier() - - return nil - } - - @objc private func reachabilityChanged(notification: NSNotification) { - let reach = notification.object - let reachability = statusFromReachability(reachability: reach as? Reachability) - eventSink?(reachability) - } - - public func onCancel(withArguments _: Any?) -> FlutterError? { - reach?.stopNotifier() - NotificationCenter.default.removeObserver(self) - eventSink = nil - return nil - } -} diff --git a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h b/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h deleted file mode 100644 index e5370fb349c3..000000000000 --- a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h +++ /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 - -#import "SystemConfiguration/CaptiveNetwork.h" - -#include -#include - -NSString* getWifiIP() { - NSString* address = @"error"; - struct ifaddrs* interfaces = NULL; - struct ifaddrs* temp_addr = NULL; - int success = 0; - - // Retrieve the current interfaces - returns 0 on success. - success = getifaddrs(&interfaces); - if (success == 0) { - // Loop through linked list of interfaces. - temp_addr = interfaces; - while (temp_addr != NULL) { - if (temp_addr->ifa_addr->sa_family == AF_INET) { - // Check if interface is en0 which is the wifi connection on the iPhone. - if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { - // Get NSString from C String - address = [NSString - stringWithUTF8String:inet_ntoa(((struct sockaddr_in*)temp_addr->ifa_addr)->sin_addr)]; - } - } - - temp_addr = temp_addr->ifa_next; - } - } - - // Free memory - freeifaddrs(interfaces); - - return address; -} diff --git a/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec b/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec deleted file mode 100644 index 51629084a23d..000000000000 --- a/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'connectivity_macos' - s.version = '0.0.1' - s.summary = 'Flutter plugin for checking connectivity' - s.description = <<-DESC - Desktop implementation of the connectivity plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_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/connectivity/connectivity_macos' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - s.dependency 'Reachability' - - s.platform = :osx - s.osx.deployment_target = '10.11' - s.swift_version = '5.0' -end diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml deleted file mode 100644 index b98f23d34eb7..000000000000 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: connectivity_macos -description: macOS implementation of the connectivity plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.2.1+2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - implements: connectivity - platforms: - macos: - pluginClass: ConnectivityPlugin - -dependencies: - flutter: - sdk: flutter - # The implementation of this plugin doesn't explicitly depend on the method channel - # defined in the platform interface. - # To prevent potential breakages, this dependency is added. - # - # In the future, this plugin's platform code should be able to reference the - # interface's platform code. (Android already supports this). - connectivity_platform_interface: ^2.0.0 - -dev_dependencies: - pedantic: ^1.10.0 diff --git a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md deleted file mode 100644 index ee75d03ccc65..000000000000 --- a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,44 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.7 - -* Update Flutter SDK constraint. - -## 1.0.6 - -* Update lower bound of dart dependency to 2.1.0. - -## 1.0.5 - -* Remove dart:io Platform checks from the MethodChannel implementation. This is -tripping the analysis of other versions of the plugin. - -## 1.0.4 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. - -## 1.0.3 - -* Make the pedantic dev_dependency explicit. - -## 1.0.2 - -* Bring ConnectivityResult and LocationAuthorizationStatus enums from the core package. -* Use the above Enums as return values for ConnectivityPlatformInterface methods. -* Modify the MethodChannel implementation so it returns the right types. -* Bring all utility methods, asserts and other logic that is only needed on the MethodChannel implementation from the core package. -* Bring MethodChannel unit tests from core package. - -## 1.0.1 - -* Fix README.md link. - -## 1.0.0 - -* Initial release. diff --git a/packages/connectivity/connectivity_platform_interface/README.md b/packages/connectivity/connectivity_platform_interface/README.md deleted file mode 100644 index 76b39315637e..000000000000 --- a/packages/connectivity/connectivity_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# connectivity_platform_interface - -A common platform interface for the [`connectivity`][1] plugin. - -This interface allows platform-specific implementations of the `connectivity` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `connectivity`, extend -[`ConnectivityPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`ConnectivityPlatform` by calling -`ConnectivityPlatform.instance = MyPlatformConnectivity()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../ -[2]: lib/connectivity_platform_interface.dart diff --git a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart b/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart deleted file mode 100644 index 6667b353a4aa..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'src/enums.dart'; -import 'src/method_channel_connectivity.dart'; - -export 'src/enums.dart'; - -/// The interface that implementations of connectivity must implement. -/// -/// Platform implementations should extend this class rather than implement it as `Connectivity` -/// 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 -/// [ConnectivityPlatform] methods. -abstract class ConnectivityPlatform extends PlatformInterface { - /// Constructs a ConnectivityPlatform. - ConnectivityPlatform() : super(token: _token); - - static final Object _token = Object(); - - static ConnectivityPlatform _instance = MethodChannelConnectivity(); - - /// The default instance of [ConnectivityPlatform] to use. - /// - /// Defaults to [MethodChannelConnectivity]. - static ConnectivityPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [ConnectivityPlatform] when they register themselves. - static set instance(ConnectivityPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Checks the connection status of the device. - Future checkConnectivity() { - throw UnimplementedError('checkConnectivity() has not been implemented.'); - } - - /// Returns a Stream of ConnectivityResults changes. - Stream get onConnectivityChanged { - throw UnimplementedError( - 'get onConnectivityChanged has not been implemented.'); - } - - /// Obtains the wifi name (SSID) of the connected network - Future getWifiName() { - throw UnimplementedError('getWifiName() has not been implemented.'); - } - - /// Obtains the wifi BSSID of the connected network. - Future getWifiBSSID() { - throw UnimplementedError('getWifiBSSID() has not been implemented.'); - } - - /// Obtains the IP address of the connected wifi network - Future getWifiIP() { - throw UnimplementedError('getWifiIP() has not been implemented.'); - } - - /// Request to authorize the location service (Only on iOS). - Future requestLocationServiceAuthorization( - {bool requestAlwaysLocationUsage = false}) { - throw UnimplementedError( - 'requestLocationServiceAuthorization() has not been implemented.'); - } - - /// Get the current location service authorization (Only on iOS). - Future getLocationServiceAuthorization() { - throw UnimplementedError( - 'getLocationServiceAuthorization() has not been implemented.'); - } -} diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart b/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart deleted file mode 100644 index b77f54cf60b2..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart +++ /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. - -/// Connection status check result. -enum ConnectivityResult { - /// WiFi: Device connected via Wi-Fi - wifi, - - /// Mobile: Device connected to cellular network - mobile, - - /// None: Device not connected to any network - none -} - -/// The status of the location service authorization. -enum LocationAuthorizationStatus { - /// The authorization of the location service is not determined. - notDetermined, - - /// This app is not authorized to use location. - restricted, - - /// User explicitly denied the location service. - denied, - - /// User authorized the app to access the location at any time. - authorizedAlways, - - /// User authorized the app to access the location when the app is visible to them. - authorizedWhenInUse, - - /// Status unknown. - unknown -} diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart b/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart deleted file mode 100644 index bdf820ac3ba7..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; - -import 'utils.dart'; - -/// An implementation of [ConnectivityPlatform] that uses method channels. -class MethodChannelConnectivity extends ConnectivityPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - MethodChannel('plugins.flutter.io/connectivity'); - - /// The event channel used to receive ConnectivityResult changes from the native platform. - @visibleForTesting - EventChannel eventChannel = - EventChannel('plugins.flutter.io/connectivity_status'); - - Stream? _onConnectivityChanged; - - /// Fires whenever the connectivity state changes. - Stream get onConnectivityChanged { - if (_onConnectivityChanged == null) { - _onConnectivityChanged = - eventChannel.receiveBroadcastStream().map((dynamic result) { - return result != null ? result.toString() : ''; - }).map(parseConnectivityResult); - } - return _onConnectivityChanged!; - } - - @override - Future checkConnectivity() async { - final String checkResult = - await methodChannel.invokeMethod('check') ?? ''; - return parseConnectivityResult(checkResult); - } - - @override - Future getWifiName() async { - String? wifiName = await methodChannel.invokeMethod('wifiName'); - // as Android might return , uniforming result - // our iOS implementation will return null - if (wifiName == '') { - wifiName = null; - } - return wifiName; - } - - @override - Future getWifiBSSID() { - return methodChannel.invokeMethod('wifiBSSID'); - } - - @override - Future getWifiIP() { - return methodChannel.invokeMethod('wifiIPAddress'); - } - - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) async { - final String requestLocationServiceResult = await methodChannel - .invokeMethod('requestLocationServiceAuthorization', - [requestAlwaysLocationUsage]) ?? - ''; - return parseLocationAuthorizationStatus(requestLocationServiceResult); - } - - @override - Future getLocationServiceAuthorization() async { - final String getLocationServiceResult = await methodChannel - .invokeMethod('getLocationServiceAuthorization') ?? - ''; - return parseLocationAuthorizationStatus(getLocationServiceResult); - } -} diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart b/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart deleted file mode 100644 index 3b5b753f6c29..000000000000 --- a/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart +++ /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 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; - -/// Convert a String to a ConnectivityResult value. -ConnectivityResult parseConnectivityResult(String state) { - switch (state) { - case 'wifi': - return ConnectivityResult.wifi; - case 'mobile': - return ConnectivityResult.mobile; - case 'none': - default: - return ConnectivityResult.none; - } -} - -/// Convert a String to a LocationAuthorizationStatus value. -LocationAuthorizationStatus parseLocationAuthorizationStatus(String result) { - switch (result) { - case 'notDetermined': - return LocationAuthorizationStatus.notDetermined; - case 'restricted': - return LocationAuthorizationStatus.restricted; - case 'denied': - return LocationAuthorizationStatus.denied; - case 'authorizedAlways': - return LocationAuthorizationStatus.authorizedAlways; - case 'authorizedWhenInUse': - return LocationAuthorizationStatus.authorizedWhenInUse; - default: - return LocationAuthorizationStatus.unknown; - } -} diff --git a/packages/connectivity/connectivity_platform_interface/pubspec.yaml b/packages/connectivity/connectivity_platform_interface/pubspec.yaml deleted file mode 100644 index 2003fdde6eeb..000000000000 --- a/packages/connectivity/connectivity_platform_interface/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: connectivity_platform_interface -description: A common platform interface for the connectivity plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%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.1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart b/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart deleted file mode 100644 index b69feae252eb..000000000000 --- a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.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. - -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity_platform_interface/src/method_channel_connectivity.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelConnectivity', () { - final List log = []; - late MethodChannelConnectivity methodChannelConnectivity; - - setUp(() async { - methodChannelConnectivity = MethodChannelConnectivity(); - - methodChannelConnectivity.methodChannel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'check': - return 'wifi'; - case 'wifiName': - return '1337wifi'; - case 'wifiBSSID': - return 'c0:ff:33:c0:d3:55'; - case 'wifiIPAddress': - return '127.0.0.1'; - case 'requestLocationServiceAuthorization': - return 'authorizedAlways'; - case 'getLocationServiceAuthorization': - return 'authorizedAlways'; - default: - return null; - } - }); - log.clear(); - MethodChannel(methodChannelConnectivity.eventChannel.name) - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'listen': - await _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - methodChannelConnectivity.eventChannel.name, - methodChannelConnectivity.eventChannel.codec - .encodeSuccessEnvelope('wifi'), - (_) {}, - ); - break; - case 'cancel': - default: - return null; - } - }); - }); - - test('onConnectivityChanged', () async { - final ConnectivityResult result = - await methodChannelConnectivity.onConnectivityChanged.first; - expect(result, ConnectivityResult.wifi); - }); - - test('getWifiName', () async { - final String? result = await methodChannelConnectivity.getWifiName(); - expect(result, '1337wifi'); - expect( - log, - [ - isMethodCall( - 'wifiName', - arguments: null, - ), - ], - ); - }); - - test('getWifiBSSID', () async { - final String? result = await methodChannelConnectivity.getWifiBSSID(); - expect(result, 'c0:ff:33:c0:d3:55'); - expect( - log, - [ - isMethodCall( - 'wifiBSSID', - arguments: null, - ), - ], - ); - }); - - test('getWifiIP', () async { - final String? result = await methodChannelConnectivity.getWifiIP(); - expect(result, '127.0.0.1'); - expect( - log, - [ - isMethodCall( - 'wifiIPAddress', - arguments: null, - ), - ], - ); - }); - - test('requestLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelConnectivity.requestLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'requestLocationServiceAuthorization', - arguments: [false], - ), - ], - ); - }); - - test('getLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelConnectivity.getLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'getLocationServiceAuthorization', - arguments: null, - ), - ], - ); - }); - - test('checkConnectivity', () async { - final ConnectivityResult result = - await methodChannelConnectivity.checkConnectivity(); - expect(result, ConnectivityResult.wifi); - expect( - log, - [ - isMethodCall( - 'check', - arguments: null, - ), - ], - ); - }); - }); -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/cross_file/analysis_options.yaml b/packages/cross_file/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/cross_file/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/device_info/analysis_options.yaml b/packages/device_info/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/device_info/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md deleted file mode 100644 index 97349d450cf1..000000000000 --- a/packages/device_info/device_info/CHANGELOG.md +++ /dev/null @@ -1,181 +0,0 @@ -## NEXT - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) - -## 1.0.1 - -* Update Flutter SDK constraint. - -## 1.0.0 - -* Announce 1.0.0. - -## 0.4.2+10 - -* Update Dart SDK constraint in example. - -## 0.4.2+9 - -* Update android compileSdkVersion to 29. - -## 0.4.2+8 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.2+7 - -* Port device_info plugin to use platform interface. - -## 0.4.2+6 - -* Moved everything from device_info to device_info/device_info - -## 0.4.2+5 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.2+4 - -Update lower bound of dart dependency to 2.1.0. - -## 0.4.2+3 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.2+2 - -* Fix CocoaPods podspec lint warnings. - -## 0.4.2+1 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Remove deprecated API usage warning in AndroidIntentPlugin.java. -* Migrates the Android example to V2 embedding. -* Bumps AGP to 3.6.1. - -## 0.4.2 - -* Add systemFeatures to AndroidDeviceInfo. - -## 0.4.1+5 - -* Make the pedantic dev_dependency explicit. - -## 0.4.1+4 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.1+3 - -* Fix pedantic errors. Adds some missing documentation and fixes unawaited - futures in the tests. - -## 0.4.1+2 - -* Remove AndroidX warning. - -## 0.4.1+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.1 - -* Support the v2 Android embedding. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - - -## 0.4.0+4 - -* Define clang module for iOS. - -## 0.4.0+3 - -* Update and migrate iOS example project. - -## 0.4.0+2 - -* Bump minimum Flutter version to 1.5.0. -* Add missing template type parameter to `invokeMethod` calls. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.0 - -* Added ability to get Android ID for Android devices - -## 0.2.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.2 - -* Fixed Dart 2 type errors. - -## 0.1.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.5 - -* Added FLT prefix to iOS types - -## 0.0.4 - -* Fixed Java/Dart communication error with empty lists - -## 0.0.3 - -* Added support for utsname - -## 0.0.2 - -* Fixed broken type comparison -* Added "isPhysicalDevice" field, detecting emulators/simulators - -## 0.0.1 - -* Implements platform-specific device/OS properties diff --git a/packages/device_info/device_info/README.md b/packages/device_info/device_info/README.md deleted file mode 100644 index 34cf9bb2ac2b..000000000000 --- a/packages/device_info/device_info/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# device_info - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`device_info_plus`](https://pub.dev/packages/device_info_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -Get current device information from within the Flutter application. - -# Usage - -Import `package:device_info/device_info.dart`, instantiate `DeviceInfoPlugin` -and use the Android and iOS getters to get platform-specific device -information. - -Example: - -```dart -import 'package:device_info/device_info.dart'; - -DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); -AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; -print('Running on ${androidInfo.model}'); // e.g. "Moto G (4)" - -IosDeviceInfo iosInfo = await deviceInfo.iosInfo; -print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1" -``` - -You will find links to the API docs on the [pub page](https://pub.dev/packages/device_info). - -## 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/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle deleted file mode 100644 index ed89da419d4a..000000000000 --- a/packages/device_info/device_info/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.deviceinfo' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/device_info/device_info/android/settings.gradle b/packages/device_info/device_info/android/settings.gradle deleted file mode 100644 index 0e75718c9a9d..000000000000 --- a/packages/device_info/device_info/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'device_info' diff --git a/packages/device_info/device_info/android/src/main/AndroidManifest.xml b/packages/device_info/device_info/android/src/main/AndroidManifest.xml deleted file mode 100644 index 03e76883266f..000000000000 --- a/packages/device_info/device_info/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java deleted file mode 100644 index 9b766d7f8381..000000000000 --- a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java +++ /dev/null @@ -1,45 +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.deviceinfo; - -import android.content.Context; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; - -/** DeviceInfoPlugin */ -public class DeviceInfoPlugin implements FlutterPlugin { - - MethodChannel channel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - DeviceInfoPlugin plugin = new DeviceInfoPlugin(); - plugin.setupMethodChannel(registrar.messenger(), registrar.context()); - } - - @Override - public void onAttachedToEngine(FlutterPlugin.FlutterPluginBinding binding) { - setupMethodChannel(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { - tearDownChannel(); - } - - private void setupMethodChannel(BinaryMessenger messenger, Context context) { - channel = new MethodChannel(messenger, "plugins.flutter.io/device_info"); - final MethodCallHandlerImpl handler = - new MethodCallHandlerImpl(context.getContentResolver(), context.getPackageManager()); - channel.setMethodCallHandler(handler); - } - - private void tearDownChannel() { - channel.setMethodCallHandler(null); - channel = null; - } -} diff --git a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java deleted file mode 100644 index 531e5db0c237..000000000000 --- a/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.deviceinfo; - -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.provider.Settings; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * The implementation of {@link MethodChannel.MethodCallHandler} for the plugin. Responsible for - * receiving method calls from method channel. - */ -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - - private final ContentResolver contentResolver; - private final PackageManager packageManager; - - /** Substitute for missing values. */ - private static final String[] EMPTY_STRING_LIST = new String[] {}; - - /** Constructs DeviceInfo. {@code contentResolver} and {@code packageManager} must not be null. */ - MethodCallHandlerImpl(ContentResolver contentResolver, PackageManager packageManager) { - this.contentResolver = contentResolver; - this.packageManager = packageManager; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - if (call.method.equals("getAndroidDeviceInfo")) { - Map build = new HashMap<>(); - build.put("board", Build.BOARD); - build.put("bootloader", Build.BOOTLOADER); - build.put("brand", Build.BRAND); - build.put("device", Build.DEVICE); - build.put("display", Build.DISPLAY); - build.put("fingerprint", Build.FINGERPRINT); - build.put("hardware", Build.HARDWARE); - build.put("host", Build.HOST); - build.put("id", Build.ID); - build.put("manufacturer", Build.MANUFACTURER); - build.put("model", Build.MODEL); - build.put("product", Build.PRODUCT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - build.put("supported32BitAbis", Arrays.asList(Build.SUPPORTED_32_BIT_ABIS)); - build.put("supported64BitAbis", Arrays.asList(Build.SUPPORTED_64_BIT_ABIS)); - build.put("supportedAbis", Arrays.asList(Build.SUPPORTED_ABIS)); - } else { - build.put("supported32BitAbis", Arrays.asList(EMPTY_STRING_LIST)); - build.put("supported64BitAbis", Arrays.asList(EMPTY_STRING_LIST)); - build.put("supportedAbis", Arrays.asList(EMPTY_STRING_LIST)); - } - build.put("tags", Build.TAGS); - build.put("type", Build.TYPE); - build.put("isPhysicalDevice", !isEmulator()); - build.put("androidId", getAndroidId()); - - build.put("systemFeatures", Arrays.asList(getSystemFeatures())); - - Map version = new HashMap<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - version.put("baseOS", Build.VERSION.BASE_OS); - version.put("previewSdkInt", Build.VERSION.PREVIEW_SDK_INT); - version.put("securityPatch", Build.VERSION.SECURITY_PATCH); - } - version.put("codename", Build.VERSION.CODENAME); - version.put("incremental", Build.VERSION.INCREMENTAL); - version.put("release", Build.VERSION.RELEASE); - version.put("sdkInt", Build.VERSION.SDK_INT); - build.put("version", version); - - result.success(build); - } else { - result.notImplemented(); - } - } - - private String[] getSystemFeatures() { - FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures(); - if (featureInfos == null) { - return EMPTY_STRING_LIST; - } - String[] features = new String[featureInfos.length]; - for (int i = 0; i < featureInfos.length; i++) { - features[i] = featureInfos[i].name; - } - return features; - } - - /** - * Returns the Android hardware device ID that is unique between the device + user and app - * signing. This key will change if the app is uninstalled or its data is cleared. Device factory - * reset will also result in a value change. - * - * @return The android ID - */ - @SuppressLint("HardwareIds") - private String getAndroidId() { - return Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID); - } - - /** - * A simple emulator-detection based on the flutter tools detection logic and a couple of legacy - * detection systems - */ - private boolean isEmulator() { - return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator"); - } -} diff --git a/packages/device_info/device_info/example/README.md b/packages/device_info/device_info/example/README.md deleted file mode 100644 index ea47551011d0..000000000000 --- a/packages/device_info/device_info/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# device_info_example - -Demonstrates how to use the `device_info` plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/device_info/device_info/example/android/app/build.gradle b/packages/device_info/device_info/example/android/app/build.gradle deleted file mode 100644 index eb0c628330be..000000000000 --- a/packages/device_info/device_info/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.deviceinfoexample" - minSdkVersion 16 - 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 - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml b/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 4268475986a3..000000000000 --- a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/device_info/device_info/example/android/build.gradle b/packages/device_info/device_info/example/android/build.gradle deleted file mode 100644 index 3274eb601b9b..000000000000 --- a/packages/device_info/device_info/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.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/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c243e25a0fff..000000000000 --- a/packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Mar 09 11:05:13 GMT 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/packages/device_info/device_info/example/integration_test/device_info_test.dart b/packages/device_info/device_info/example/integration_test/device_info_test.dart deleted file mode 100644 index 953eb856d62a..000000000000 --- a/packages/device_info/device_info/example/integration_test/device_info_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:device_info/device_info.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - late IosDeviceInfo iosInfo; - late AndroidDeviceInfo androidInfo; - - setUpAll(() async { - final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - if (Platform.isIOS) { - iosInfo = await deviceInfoPlugin.iosInfo; - } else if (Platform.isAndroid) { - androidInfo = await deviceInfoPlugin.androidInfo; - } - }); - - testWidgets('Can get non-null device model', (WidgetTester tester) async { - if (Platform.isIOS) { - expect(iosInfo.model, isNotNull); - } else if (Platform.isAndroid) { - expect(androidInfo.model, isNotNull); - } - }); -} diff --git a/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist b/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/device_info/device_info/example/ios/Podfile b/packages/device_info/device_info/example/ios/Podfile deleted file mode 100644 index f7d6a5e68c3a..000000000000 --- a/packages/device_info/device_info/example/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# 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__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index ca599db5b7ac..000000000000 --- a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,460 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17470FCDF9FA37CB94B63753 /* 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 */; }; -/* 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 = ""; }; - 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 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 = ""; }; - C8043F3EE7ED1716F368CC90 /* 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 = ""; }; - E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 20A0DD43C00A880430740858 /* Pods */ = { - isa = PBXGroup; - children = ( - C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */, - E96AF818D5456D130681C78B /* Pods-Runner.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 = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 20A0DD43C00A880430740858 /* Pods */, - EA17DAB2B097E79A4CABE344 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - EA17DAB2B097E79A4CABE344 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B8856E1B697C88C1C62EE937 /* [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 = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" 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"; - }; - B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; - 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.deviceInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/device_info/example/ios/Runner/Info.plist b/packages/device_info/device_info/example/ios/Runner/Info.plist deleted file mode 100644 index 1ea53f8dc54b..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - device_info_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/device_info/device_info/example/ios/Runner/main.m b/packages/device_info/device_info/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/device_info/device_info/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/device_info/device_info/example/lib/main.dart b/packages/device_info/device_info/example/lib/main.dart deleted file mode 100644 index 44e3fb4ee2f7..000000000000 --- a/packages/device_info/device_info/example/lib/main.dart +++ /dev/null @@ -1,146 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:device_info/device_info.dart'; - -void main() { - runZonedGuarded(() { - runApp(MyApp()); - }, (dynamic error, dynamic stack) { - print(error); - print(stack); - }); -} - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - Map _deviceData = {}; - - @override - void initState() { - super.initState(); - initPlatformState(); - } - - Future initPlatformState() async { - Map deviceData = {}; - - try { - if (Platform.isAndroid) { - deviceData = _readAndroidBuildData(await deviceInfoPlugin.androidInfo); - } else if (Platform.isIOS) { - deviceData = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo); - } - } on PlatformException { - deviceData = { - 'Error:': 'Failed to get platform version.' - }; - } - - if (!mounted) return; - - setState(() { - _deviceData = deviceData; - }); - } - - Map _readAndroidBuildData(AndroidDeviceInfo build) { - return { - 'version.securityPatch': build.version.securityPatch, - 'version.sdkInt': build.version.sdkInt, - 'version.release': build.version.release, - 'version.previewSdkInt': build.version.previewSdkInt, - 'version.incremental': build.version.incremental, - 'version.codename': build.version.codename, - 'version.baseOS': build.version.baseOS, - 'board': build.board, - 'bootloader': build.bootloader, - 'brand': build.brand, - 'device': build.device, - 'display': build.display, - 'fingerprint': build.fingerprint, - 'hardware': build.hardware, - 'host': build.host, - 'id': build.id, - 'manufacturer': build.manufacturer, - 'model': build.model, - 'product': build.product, - 'supported32BitAbis': build.supported32BitAbis, - 'supported64BitAbis': build.supported64BitAbis, - 'supportedAbis': build.supportedAbis, - 'tags': build.tags, - 'type': build.type, - 'isPhysicalDevice': build.isPhysicalDevice, - 'androidId': build.androidId, - 'systemFeatures': build.systemFeatures, - }; - } - - Map _readIosDeviceInfo(IosDeviceInfo data) { - return { - 'name': data.name, - 'systemName': data.systemName, - 'systemVersion': data.systemVersion, - 'model': data.model, - 'localizedModel': data.localizedModel, - 'identifierForVendor': data.identifierForVendor, - 'isPhysicalDevice': data.isPhysicalDevice, - 'utsname.sysname:': data.utsname.sysname, - 'utsname.nodename:': data.utsname.nodename, - 'utsname.release:': data.utsname.release, - 'utsname.version:': data.utsname.version, - 'utsname.machine:': data.utsname.machine, - }; - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: Text( - Platform.isAndroid ? 'Android Device Info' : 'iOS Device Info'), - ), - body: ListView( - children: _deviceData.keys.map((String property) { - return Row( - children: [ - Container( - padding: const EdgeInsets.all(10.0), - child: Text( - property, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), - child: Text( - '${_deviceData[property]}', - maxLines: 10, - overflow: TextOverflow.ellipsis, - ), - )), - ], - ); - }).toList(), - ), - ), - ); - } -} diff --git a/packages/device_info/device_info/example/pubspec.yaml b/packages/device_info/device_info/example/pubspec.yaml deleted file mode 100644 index c4e84f60de5e..000000000000 --- a/packages/device_info/device_info/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: device_info_example -description: Demonstrates how to use the device_info plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - device_info: - # When depending on this package from a real application you should use: - # device_info: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h deleted file mode 100644 index 511b5b893fcb..000000000000 --- a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTDeviceInfoPlugin : NSObject -@end diff --git a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m deleted file mode 100644 index 3d4d25ad9c6e..000000000000 --- a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m +++ /dev/null @@ -1,55 +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 "FLTDeviceInfoPlugin.h" -#import - -@implementation FLTDeviceInfoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/device_info" - binaryMessenger:[registrar messenger]]; - FLTDeviceInfoPlugin* instance = [[FLTDeviceInfoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([@"getIosDeviceInfo" isEqualToString:call.method]) { - UIDevice* device = [UIDevice currentDevice]; - struct utsname un; - uname(&un); - - result(@{ - @"name" : [device name], - @"systemName" : [device systemName], - @"systemVersion" : [device systemVersion], - @"model" : [device model], - @"localizedModel" : [device localizedModel], - @"identifierForVendor" : [[device identifierForVendor] UUIDString], - @"isPhysicalDevice" : [self isDevicePhysical], - @"utsname" : @{ - @"sysname" : @(un.sysname), - @"nodename" : @(un.nodename), - @"release" : @(un.release), - @"version" : @(un.version), - @"machine" : @(un.machine), - } - }); - } else { - result(FlutterMethodNotImplemented); - } -} - -// return value is false if code is run on a simulator -- (NSString*)isDevicePhysical { -#if TARGET_OS_SIMULATOR - NSString* isPhysicalDevice = @"false"; -#else - NSString* isPhysicalDevice = @"true"; -#endif - - return isPhysicalDevice; -} - -@end diff --git a/packages/device_info/device_info/ios/device_info.podspec b/packages/device_info/device_info/ios/device_info.podspec deleted file mode 100644 index 1d1baa4787f6..000000000000 --- a/packages/device_info/device_info/ios/device_info.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'device_info' - s.version = '0.0.1' - s.summary = 'Flutter Device Info' - s.description = <<-DESC -Get current device information from within the Flutter application. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/device_info' } - s.documentation_url = 'https://pub.dev/packages/device_info' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/device_info/device_info/lib/device_info.dart b/packages/device_info/device_info/lib/device_info.dart deleted file mode 100644 index 1153ac6f7da6..000000000000 --- a/packages/device_info/device_info/lib/device_info.dart +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:device_info_platform_interface/device_info_platform_interface.dart'; - -export 'package:device_info_platform_interface/device_info_platform_interface.dart' - show AndroidBuildVersion, AndroidDeviceInfo, IosDeviceInfo, IosUtsname; - -/// Provides device and operating system information. -class DeviceInfoPlugin { - /// No work is done when instantiating the plugin. It's safe to call this - /// repeatedly or in performance-sensitive blocks. - DeviceInfoPlugin(); - - /// This information does not change from call to call. Cache it. - AndroidDeviceInfo? _cachedAndroidDeviceInfo; - - /// Information derived from `android.os.Build`. - /// - /// See: https://developer.android.com/reference/android/os/Build.html - Future get androidInfo async => - _cachedAndroidDeviceInfo ??= - await DeviceInfoPlatform.instance.androidInfo(); - - /// This information does not change from call to call. Cache it. - IosDeviceInfo? _cachedIosDeviceInfo; - - /// Information derived from `UIDevice`. - /// - /// See: https://developer.apple.com/documentation/uikit/uidevice - Future get iosInfo async => - _cachedIosDeviceInfo ??= await DeviceInfoPlatform.instance.iosInfo(); -} diff --git a/packages/device_info/device_info/pubspec.yaml b/packages/device_info/device_info/pubspec.yaml deleted file mode 100644 index c5830f401039..000000000000 --- a/packages/device_info/device_info/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: device_info -description: Flutter plugin providing detailed information about the device - (make, model, etc.), and Android or iOS version the app is running on. -repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%22 -version: 2.0.2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.deviceinfo - pluginClass: DeviceInfoPlugin - ios: - pluginClass: FLTDeviceInfoPlugin - -dependencies: - flutter: - sdk: flutter - device_info_platform_interface: ^2.0.0 -dev_dependencies: - test: ^1.16.3 - flutter_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_platform_interface/CHANGELOG.md b/packages/device_info/device_info_platform_interface/CHANGELOG.md deleted file mode 100644 index 438d5bccad40..000000000000 --- a/packages/device_info/device_info_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,21 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. -* Make `baseOS`, `previewSdkInt`, and `securityPatch` nullable types. -* Remove default values for non-nullable types. - -## 1.0.2 - -- Update Flutter SDK constraint. - -## 1.0.1 - -- Documentation typo fixed. - -## 1.0.0 - -- Initial open-source release. diff --git a/packages/device_info/device_info_platform_interface/README.md b/packages/device_info/device_info_platform_interface/README.md deleted file mode 100644 index 1391ffded5ee..000000000000 --- a/packages/device_info/device_info_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# device_info_platform_interface - -A common platform interface for the [`device_info`][1] plugin. - -This interface allows platform-specific implementations of the `device_info` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `device_info`, extend -[`DeviceInfoPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`DeviceInfoPlatform` by calling -`DeviceInfoPlatform.instance = MyPlatformDeviceInfo()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../device_info -[2]: lib/device_info_platform_interface.dart diff --git a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart deleted file mode 100644 index a40363b2dcb6..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'method_channel/method_channel_device_info.dart'; -import 'model/android_device_info.dart'; -import 'model/ios_device_info.dart'; -export 'model/android_device_info.dart'; -export 'model/ios_device_info.dart'; - -/// The interface that implementations of device_info must implement. -/// -/// Platform implementations should extend this class rather than implement it as `device_info` -/// 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 -/// [DeviceInfoPlatform] methods. -abstract class DeviceInfoPlatform extends PlatformInterface { - /// Constructs a DeviceInfoPlatform. - DeviceInfoPlatform() : super(token: _token); - - static final Object _token = Object(); - - static DeviceInfoPlatform _instance = MethodChannelDeviceInfo(); - - /// The default instance of [DeviceInfoPlatform] to use. - /// - /// Defaults to [MethodChannelDeviceInfo]. - static DeviceInfoPlatform get instance => _instance; - - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [DeviceInfoPlatform] when they register themselves. - static set instance(DeviceInfoPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Gets the Android device information. - Future androidInfo() { - throw UnimplementedError('androidInfo() has not been implemented.'); - } - - /// Gets the iOS device information. - Future iosInfo() { - throw UnimplementedError('iosInfo() has not been implemented.'); - } -} diff --git a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart deleted file mode 100644 index 3c19e57f66a8..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; -import 'package:device_info_platform_interface/device_info_platform_interface.dart'; - -/// An implementation of [DeviceInfoPlatform] that uses method channels. -class MethodChannelDeviceInfo extends DeviceInfoPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel channel = MethodChannel('plugins.flutter.io/device_info'); - - // Method channel for Android devices - Future androidInfo() async { - return AndroidDeviceInfo.fromMap((await channel - .invokeMapMethod('getAndroidDeviceInfo')) ?? - {}); - } - - // Method channel for iOS devices - Future iosInfo() async { - return IosDeviceInfo.fromMap( - (await channel.invokeMapMethod('getIosDeviceInfo')) ?? - {}); - } -} diff --git a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart deleted file mode 100644 index b61dc14a0420..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart +++ /dev/null @@ -1,247 +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. - -/// Information derived from `android.os.Build`. -/// -/// See: https://developer.android.com/reference/android/os/Build.html -class AndroidDeviceInfo { - /// Android device Info class. - AndroidDeviceInfo({ - required this.version, - required this.board, - required this.bootloader, - required this.brand, - required this.device, - required this.display, - required this.fingerprint, - required this.hardware, - required this.host, - required this.id, - required this.manufacturer, - required this.model, - required this.product, - required List supported32BitAbis, - required List supported64BitAbis, - required List supportedAbis, - required this.tags, - required this.type, - required this.isPhysicalDevice, - required this.androidId, - required List systemFeatures, - }) : supported32BitAbis = List.unmodifiable(supported32BitAbis), - supported64BitAbis = List.unmodifiable(supported64BitAbis), - supportedAbis = List.unmodifiable(supportedAbis), - systemFeatures = List.unmodifiable(systemFeatures); - - /// Android operating system version values derived from `android.os.Build.VERSION`. - final AndroidBuildVersion version; - - /// The name of the underlying board, like "goldfish". - /// - /// The value is an empty String if it is not available. - final String board; - - /// The system bootloader version number. - /// - /// The value is an empty String if it is not available. - final String bootloader; - - /// The consumer-visible brand with which the product/hardware will be associated, if any. - /// - /// The value is an empty String if it is not available. - final String brand; - - /// The name of the industrial design. - /// - /// The value is an empty String if it is not available. - final String device; - - /// A build ID string meant for displaying to the user. - /// - /// The value is an empty String if it is not available. - final String display; - - /// A string that uniquely identifies this build. - /// - /// The value is an empty String if it is not available. - final String fingerprint; - - /// The name of the hardware (from the kernel command line or /proc). - /// - /// The value is an empty String if it is not available. - final String hardware; - - /// Hostname. - /// - /// The value is an empty String if it is not available. - final String host; - - /// Either a changelist number, or a label like "M4-rc20". - /// - /// The value is an empty String if it is not available. - final String id; - - /// The manufacturer of the product/hardware. - /// - /// The value is an empty String if it is not available. - final String manufacturer; - - /// The end-user-visible name for the end product. - /// - /// The value is an empty String if it is not available. - final String model; - - /// The name of the overall product. - /// - /// The value is an empty String if it is not available. - final String product; - - /// An ordered list of 32 bit ABIs supported by this device. - final List supported32BitAbis; - - /// An ordered list of 64 bit ABIs supported by this device. - final List supported64BitAbis; - - /// An ordered list of ABIs supported by this device. - final List supportedAbis; - - /// Comma-separated tags describing the build, like "unsigned,debug". - /// - /// The value is an empty String if it is not available. - final String tags; - - /// The type of build, like "user" or "eng". - /// - /// The value is an empty String if it is not available. - final String type; - - /// The value is `true` if the application is running on a physical device. - /// - /// The value is `false` when the application is running on a emulator, or the value is unavailable. - final bool isPhysicalDevice; - - /// The Android hardware device ID that is unique between the device + user and app signing. - /// - /// The value is an empty String if it is not available. - final String androidId; - - /// Describes what features are available on the current device. - /// - /// This can be used to check if the device has, for example, a front-facing - /// camera, or a touchscreen. However, in many cases this is not the best - /// API to use. For example, if you are interested in bluetooth, this API - /// can tell you if the device has a bluetooth radio, but it cannot tell you - /// if bluetooth is currently enabled, or if you have been granted the - /// necessary permissions to use it. Please *only* use this if there is no - /// other way to determine if a feature is supported. - /// - /// This data comes from Android's PackageManager.getSystemAvailableFeatures, - /// and many of the common feature strings to look for are available in - /// PackageManager's public documentation: - /// https://developer.android.com/reference/android/content/pm/PackageManager - final List systemFeatures; - - /// Deserializes from the message received from [_kChannel]. - static AndroidDeviceInfo fromMap(Map map) { - return AndroidDeviceInfo( - version: AndroidBuildVersion._fromMap(map['version'] != null - ? map['version'].cast() - : {}), - board: map['board'] ?? '', - bootloader: map['bootloader'] ?? '', - brand: map['brand'] ?? '', - device: map['device'] ?? '', - display: map['display'] ?? '', - fingerprint: map['fingerprint'] ?? '', - hardware: map['hardware'] ?? '', - host: map['host'] ?? '', - id: map['id'] ?? '', - manufacturer: map['manufacturer'] ?? '', - model: map['model'] ?? '', - product: map['product'] ?? '', - supported32BitAbis: _fromList(map['supported32BitAbis']), - supported64BitAbis: _fromList(map['supported64BitAbis']), - supportedAbis: _fromList(map['supportedAbis']), - tags: map['tags'] ?? '', - type: map['type'] ?? '', - isPhysicalDevice: map['isPhysicalDevice'] ?? false, - androidId: map['androidId'] ?? '', - systemFeatures: _fromList(map['systemFeatures']), - ); - } - - /// Deserializes message as List - static List _fromList(dynamic message) { - if (message == null) { - return []; - } - assert(message is List); - final List list = List.from(message) - ..removeWhere((value) => value == null); - return list.cast(); - } -} - -/// Version values of the current Android operating system build derived from -/// `android.os.Build.VERSION`. -/// -/// See: https://developer.android.com/reference/android/os/Build.VERSION.html -class AndroidBuildVersion { - AndroidBuildVersion._({ - this.baseOS, - this.previewSdkInt, - this.securityPatch, - required this.codename, - required this.incremental, - required this.release, - required this.sdkInt, - }); - - /// The base OS build the product is based on. - /// This is only available on Android 6.0 or above. - String? baseOS; - - /// The developer preview revision of a prerelease SDK. - /// This is only available on Android 6.0 or above. - int? previewSdkInt; - - /// The user-visible security patch level. - /// This is only available on Android 6.0 or above. - final String? securityPatch; - - /// The current development codename, or the string "REL" if this is a release build. - /// - /// The value is an empty String if it is not available. - final String codename; - - /// The internal value used by the underlying source control to represent this build. - /// - /// The value is an empty String if it is not available. - final String incremental; - - /// The user-visible version string. - /// - /// The value is an empty String if it is not available. - final String release; - - /// The user-visible SDK version of the framework. - /// - /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html - /// - /// The value is -1 if it is unavailable. - final int sdkInt; - - /// Deserializes from the map message received from [_kChannel]. - static AndroidBuildVersion _fromMap(Map map) { - return AndroidBuildVersion._( - baseOS: map['baseOS'], - previewSdkInt: map['previewSdkInt'], - securityPatch: map['securityPatch'], - codename: map['codename'] ?? '', - incremental: map['incremental'] ?? '', - release: map['release'] ?? '', - sdkInt: map['sdkInt'] ?? -1, - ); - } -} diff --git a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart deleted file mode 100644 index 17c96a3e250a..000000000000 --- a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart +++ /dev/null @@ -1,126 +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. - -/// Information derived from `UIDevice`. -/// -/// See: https://developer.apple.com/documentation/uikit/uidevice -class IosDeviceInfo { - /// IOS device info class. - IosDeviceInfo({ - required this.name, - required this.systemName, - required this.systemVersion, - required this.model, - required this.localizedModel, - required this.identifierForVendor, - required this.isPhysicalDevice, - required this.utsname, - }); - - /// Device name. - /// - /// The value is an empty String if it is not available. - final String name; - - /// The name of the current operating system. - /// - /// The value is an empty String if it is not available. - final String systemName; - - /// The current operating system version. - /// - /// The value is an empty String if it is not available. - final String systemVersion; - - /// Device model. - /// - /// The value is an empty String if it is not available. - final String model; - - /// Localized name of the device model. - /// - /// The value is an empty String if it is not available. - final String localizedModel; - - /// Unique UUID value identifying the current device. - /// - /// The value is an empty String if it is not available. - final String identifierForVendor; - - /// The value is `true` if the application is running on a physical device. - /// - /// The value is `false` when the application is running on a simulator, or the value is unavailable. - final bool isPhysicalDevice; - - /// Operating system information derived from `sys/utsname.h`. - /// - /// The value is an empty String if it is not available. - final IosUtsname utsname; - - /// Deserializes from the map message received from [_kChannel]. - static IosDeviceInfo fromMap(Map map) { - return IosDeviceInfo( - name: map['name'] ?? '', - systemName: map['systemName'] ?? '', - systemVersion: map['systemVersion'] ?? '', - model: map['model'] ?? '', - localizedModel: map['localizedModel'] ?? '', - identifierForVendor: map['identifierForVendor'] ?? '', - isPhysicalDevice: map['isPhysicalDevice'] != null - ? map['isPhysicalDevice'] == 'true' - : false, - utsname: IosUtsname._fromMap(map['utsname'] != null - ? map['utsname'].cast() - : {}), - ); - } -} - -/// Information derived from `utsname`. -/// See http://pubs.opengroup.org/onlinepubs/7908799/xsh/sysutsname.h.html for details. -class IosUtsname { - IosUtsname._({ - required this.sysname, - required this.nodename, - required this.release, - required this.version, - required this.machine, - }); - - /// Operating system name. - /// - /// The value is an empty String if it is not available. - final String sysname; - - /// Network node name. - /// - /// The value is an empty String if it is not available. - final String nodename; - - /// Release level. - /// - /// The value is an empty String if it is not available. - final String release; - - /// Version level. - /// - /// The value is an empty String if it is not available. - final String version; - - /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus). - /// - /// The value is an empty String if it is not available. - final String machine; - - /// Deserializes from the map message received from [_kChannel]. - static IosUtsname _fromMap(Map map) { - return IosUtsname._( - sysname: map['sysname'] ?? '', - nodename: map['nodename'] ?? '', - release: map['release'] ?? '', - version: map['version'] ?? '', - machine: map['machine'] ?? '', - ); - } -} diff --git a/packages/device_info/device_info_platform_interface/pubspec.yaml b/packages/device_info/device_info_platform_interface/pubspec.yaml deleted file mode 100644 index cf3e50f98422..000000000000 --- a/packages/device_info/device_info_platform_interface/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: device_info_platform_interface -description: A common platform interface for the device_info plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%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.1 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4" - -dependencies: - flutter: - sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - test: ^1.16.3 - pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart deleted file mode 100644 index 14ed7c0aefb4..000000000000 --- a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart +++ /dev/null @@ -1,373 +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:device_info_platform_interface/device_info_platform_interface.dart'; -import 'package:device_info_platform_interface/method_channel/method_channel_device_info.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group("$MethodChannelDeviceInfo", () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return ({ - "version": { - "securityPatch": "2018-09-05", - "sdkInt": 28, - "release": "9", - "previewSdkInt": 0, - "incremental": "5124027", - "codename": "REL", - "baseOS": "", - }, - "board": "goldfish_x86_64", - "bootloader": "unknown", - "brand": "google", - "device": "generic_x86_64", - "display": "PSR1.180720.075", - "fingerprint": - "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys", - "hardware": "ranchu", - "host": "abfarm730", - "id": "PSR1.180720.075", - "manufacturer": "Google", - "model": "Android SDK built for x86_64", - "product": "sdk_gphone_x86_64", - "supported32BitAbis": [ - "x86", - ], - "supported64BitAbis": [ - "x86_64", - ], - "supportedAbis": [ - "x86_64", - "x86", - ], - "tags": "release-keys", - "type": "user", - "isPhysicalDevice": false, - "androidId": "f47571f3b4648f45", - "systemFeatures": [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen", - ], - }); - case 'getIosDeviceInfo': - return ({ - "name": "iPhone 13", - "systemName": "iOS", - "systemVersion": "13.0", - "model": "iPhone", - "localizedModel": "iPhone", - "identifierForVendor": "88F59280-55AD-402C-B922-3203B4794C06", - "isPhysicalDevice": false, - "utsname": { - "sysname": "Darwin", - "nodename": "host", - "release": "19.6.0", - "version": - "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64", - "machine": "x86_64", - } - }); - default: - return null; - } - }); - }); - - test("androidInfo", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - - expect(result.version.securityPatch, "2018-09-05"); - expect(result.version.sdkInt, 28); - expect(result.version.release, "9"); - expect(result.version.previewSdkInt, 0); - expect(result.version.incremental, "5124027"); - expect(result.version.codename, "REL"); - expect(result.board, "goldfish_x86_64"); - expect(result.bootloader, "unknown"); - expect(result.brand, "google"); - expect(result.device, "generic_x86_64"); - expect(result.display, "PSR1.180720.075"); - expect(result.fingerprint, - "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys"); - expect(result.hardware, "ranchu"); - expect(result.host, "abfarm730"); - expect(result.id, "PSR1.180720.075"); - expect(result.manufacturer, "Google"); - expect(result.model, "Android SDK built for x86_64"); - expect(result.product, "sdk_gphone_x86_64"); - expect(result.supported32BitAbis, [ - "x86", - ]); - expect(result.supported64BitAbis, [ - "x86_64", - ]); - expect(result.supportedAbis, [ - "x86_64", - "x86", - ]); - expect(result.tags, "release-keys"); - expect(result.type, "user"); - expect(result.isPhysicalDevice, false); - expect(result.androidId, "f47571f3b4648f45"); - expect(result.systemFeatures, [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen", - ]); - }); - - test("iosInfo", () async { - final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, "iPhone 13"); - expect(result.systemName, "iOS"); - expect(result.systemVersion, "13.0"); - expect(result.model, "iPhone"); - expect(result.localizedModel, "iPhone"); - expect( - result.identifierForVendor, "88F59280-55AD-402C-B922-3203B4794C06"); - expect(result.isPhysicalDevice, false); - expect(result.utsname.sysname, "Darwin"); - expect(result.utsname.nodename, "host"); - expect(result.utsname.release, "19.6.0"); - expect(result.utsname.version, - "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64"); - expect(result.utsname.machine, "x86_64"); - }); - }); - - group( - "$MethodChannelDeviceInfo handles null value in the map returned from method channel", - () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return ({ - "version": null, - "board": null, - "bootloader": null, - "brand": null, - "device": null, - "display": null, - "fingerprint": null, - "hardware": null, - "host": null, - "id": null, - "manufacturer": null, - "model": null, - "product": null, - "supported32BitAbis": null, - "supported64BitAbis": null, - "supportedAbis": null, - "tags": null, - "type": null, - "isPhysicalDevice": null, - "androidId": null, - "systemFeatures": null, - }); - case 'getIosDeviceInfo': - return ({ - "name": null, - "systemName": null, - "systemVersion": null, - "model": null, - "localizedModel": null, - "identifierForVendor": null, - "isPhysicalDevice": null, - "utsname": null, - }); - default: - return null; - } - }); - }); - - test("androidInfo hanels null", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - - expect(result.version.securityPatch, null); - expect(result.version.sdkInt, -1); - expect(result.version.release, ''); - expect(result.version.previewSdkInt, null); - expect(result.version.incremental, ''); - expect(result.version.codename, ''); - expect(result.board, ''); - expect(result.bootloader, ''); - expect(result.brand, ''); - expect(result.device, ''); - expect(result.display, ''); - expect(result.fingerprint, ''); - expect(result.hardware, ''); - expect(result.host, ''); - expect(result.id, ''); - expect(result.manufacturer, ''); - expect(result.model, ''); - expect(result.product, ''); - expect(result.supported32BitAbis, []); - expect(result.supported64BitAbis, []); - expect(result.supportedAbis, []); - expect(result.tags, ''); - expect(result.type, ''); - expect(result.isPhysicalDevice, false); - expect(result.androidId, ''); - expect(result.systemFeatures, []); - }); - - test("iosInfo handles null", () async { - final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, ''); - expect(result.systemName, ''); - expect(result.systemVersion, ''); - expect(result.model, ''); - expect(result.localizedModel, ''); - expect(result.identifierForVendor, ''); - expect(result.isPhysicalDevice, false); - expect(result.utsname.sysname, ''); - expect(result.utsname.nodename, ''); - expect(result.utsname.release, ''); - expect(result.utsname.version, ''); - expect(result.utsname.machine, ''); - }); - }); - - group("$MethodChannelDeviceInfo handles method channel returns null", () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return null; - case 'getIosDeviceInfo': - return null; - default: - return null; - } - }); - }); - - test("androidInfo handles null", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - - expect(result.version.securityPatch, null); - expect(result.version.sdkInt, -1); - expect(result.version.release, ''); - expect(result.version.previewSdkInt, null); - expect(result.version.incremental, ''); - expect(result.version.codename, ''); - expect(result.board, ''); - expect(result.bootloader, ''); - expect(result.brand, ''); - expect(result.device, ''); - expect(result.display, ''); - expect(result.fingerprint, ''); - expect(result.hardware, ''); - expect(result.host, ''); - expect(result.id, ''); - expect(result.manufacturer, ''); - expect(result.model, ''); - expect(result.product, ''); - expect(result.supported32BitAbis, []); - expect(result.supported64BitAbis, []); - expect(result.supportedAbis, []); - expect(result.tags, ''); - expect(result.type, ''); - expect(result.isPhysicalDevice, false); - expect(result.androidId, ''); - expect(result.systemFeatures, []); - }); - - test("iosInfo handles null", () async { - final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, ''); - expect(result.systemName, ''); - expect(result.systemVersion, ''); - expect(result.model, ''); - expect(result.localizedModel, ''); - expect(result.identifierForVendor, ''); - expect(result.isPhysicalDevice, false); - expect(result.utsname.sysname, ''); - expect(result.utsname.nodename, ''); - expect(result.utsname.release, ''); - expect(result.utsname.version, ''); - expect(result.utsname.machine, ''); - }); - }); - - group("$MethodChannelDeviceInfo android handles null values in list", () { - late MethodChannelDeviceInfo methodChannelDeviceInfo; - - setUp(() async { - methodChannelDeviceInfo = MethodChannelDeviceInfo(); - - methodChannelDeviceInfo.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - switch (methodCall.method) { - case 'getAndroidDeviceInfo': - return ({ - "supported32BitAbis": ["x86"], - "supported64BitAbis": ["x86_64"], - "supportedAbis": ["x86_64", "x86"], - "systemFeatures": [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen", - ], - }); - default: - return null; - } - }); - }); - - test("androidInfo hanels null in list", () async { - final AndroidDeviceInfo result = - await methodChannelDeviceInfo.androidInfo(); - expect(result.supported32BitAbis, ['x86']); - expect(result.supported64BitAbis, ['x86_64']); - expect(result.supportedAbis, ['x86_64', 'x86']); - expect(result.systemFeatures, [ - "android.hardware.sensor.proximity", - "android.software.adoptable_storage", - "android.hardware.sensor.accelerometer", - "android.hardware.faketouch", - "android.software.backup", - "android.hardware.touchscreen" - ]); - }); - }); -} diff --git a/packages/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 e00ea7065ce0..8f41a8ddb2ae 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,6 +1,27 @@ -## NEXT +## 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 * Updated Android lint settings. +* Updated package description. ## 0.1.0+3 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 da0cd2ebfee8..7fafa0b309ec 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { minSdkVersion 16 @@ -49,12 +49,12 @@ android { } dependencies { - implementation 'com.google.guava:guava:28.1-android' - implementation 'com.squareup.okhttp3:okhttp:3.12.1' + implementation 'com.google.guava:guava:31.1-android' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.google.code.gson:gson:2.8.6' 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 +67,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 6def13f65898..21a59edcba40 100644 --- a/packages/espresso/example/android/app/build.gradle +++ b/packages/espresso/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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -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/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..c896585be839 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.8.0" dependencies: flutter: diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 6295c0ce9694..644d0ab5980a 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -1,12 +1,13 @@ name: espresso description: Java classes for testing Flutter apps using Espresso. -repository: https://github.com/flutter/plugins/tree/master/packages/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+3 +version: 0.2.0+3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: diff --git a/packages/file_selector/analysis_options.yaml b/packages/file_selector/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/file_selector/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml 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 2f8a4d0754f1..db29f829771d 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,6 +1,40 @@ +## NEXT + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 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. + +## 0.8.2+1 + +* Minor code cleanup for new analysis rules. +* Updated package description. + ## 0.8.2 -* Update platform_plugin_interface version requirement. +* Update `platform_plugin_interface` version requirement. ## 0.8.1 diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md index 22ae7073ca2d..f5c1de8afebc 100644 --- a/packages/file_selector/file_selector/README.md +++ b/packages/file_selector/file_selector/README.md @@ -1,36 +1,80 @@ # 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. +| | macOS | Web | Windows | +|-------------|--------|-----|-------------| +| **Support** | 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]); +final 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]); +final XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], +); +final XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], +); +final List files = await openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, +]); ``` #### Saving a file + ```dart -final path = await getSavePath(); -final name = "hello_file_selector.txt"; -final data = Uint8List.fromList("Hello World!".codeUnits); -final mimeType = "text/plain"; -final file = XFile.fromData(data, name: name, mimeType: mimeType); -await file.saveTo(path); +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); ``` [example]:./example +[entitlement]: https://docs.flutter.dev/desktop#entitlements-and-the-app-sandbox 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/lib/get_directory_page.dart b/packages/file_selector/file_selector/example/lib/get_directory_page.dart index 6022559ce40e..1a36be3fe26b 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 @@ -7,8 +7,11 @@ import 'package:flutter/material.dart'; /// Screen that shows an example of getDirectoryPath class GetDirectoryPage extends StatelessWidget { - void _getDirectoryPath(BuildContext context) async { - final String confirmButtonText = 'Choose'; + /// Default Constructor + const GetDirectoryPage({Key? key}) : super(key: key); + + Future _getDirectoryPath(BuildContext context) async { + const String confirmButtonText = 'Choose'; final String? directoryPath = await getDirectoryPath( confirmButtonText: confirmButtonText, ); @@ -16,9 +19,9 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( + await showDialog( context: context, - builder: (context) => TextDisplay(directoryPath), + builder: (BuildContext context) => TextDisplay(directoryPath), ); } @@ -26,7 +29,7 @@ class GetDirectoryPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open a text file"), + title: const Text('Open a text file'), ), body: Center( child: Column( @@ -34,10 +37,13 @@ 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: Text('Press to ask user to choose a directory'), + child: const Text('Press to ask user to choose a directory'), onPressed: () => _getDirectoryPath(context), ), ], @@ -49,22 +55,22 @@ class GetDirectoryPage extends StatelessWidget { /// Widget that displays a text file in a dialog class TextDisplay extends StatelessWidget { + /// Default Constructor + const TextDisplay(this.directoryPath, {Key? key}) : super(key: key); + /// Directory path final String directoryPath; - /// Default Constructor - TextDisplay(this.directoryPath); - @override Widget build(BuildContext context) { return AlertDialog( - title: Text('Selected Directory'), + title: const Text('Selected Directory'), content: Scrollbar( child: SingleChildScrollView( child: Text(directoryPath), ), ), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () => Navigator.pop(context), diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart index ab0b5c32187c..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,15 +6,21 @@ 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: Text('File Selector Demo Home Page'), + title: const Text('File Selector Demo Home Page'), ), body: Center( child: Column( @@ -22,31 +28,31 @@ class HomePage extends StatelessWidget { children: [ ElevatedButton( style: style, - child: Text('Open a text file'), + child: const Text('Open a text file'), onPressed: () => Navigator.pushNamed(context, '/open/text'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Open an image'), + child: const Text('Open an image'), onPressed: () => Navigator.pushNamed(context, '/open/image'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Open multiple images'), + child: const Text('Open multiple images'), onPressed: () => Navigator.pushNamed(context, '/open/images'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Save a file'), + child: const Text('Save a file'), onPressed: () => Navigator.pushNamed(context, '/save/text'), ), - SizedBox(height: 10), + const SizedBox(height: 10), ElevatedButton( style: style, - child: Text('Open a get directory dialog'), + child: const Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), ], diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart index bd4c9b7ab853..d05e80f1b755 100644 --- a/packages/file_selector/file_selector/example/lib/main.dart +++ b/packages/file_selector/file_selector/example/lib/main.dart @@ -3,19 +3,23 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:example/home_page.dart'; -import 'package:example/get_directory_page.dart'; -import 'package:example/open_text_page.dart'; -import 'package:example/open_image_page.dart'; -import 'package:example/open_multiple_images_page.dart'; -import 'package:example/save_text_page.dart'; + +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(), - routes: { - '/open/image': (context) => OpenImagePage(), - '/open/images': (context) => OpenMultipleImagesPage(), - '/open/text': (context) => OpenTextPage(), - '/save/text': (context) => SaveTextPage(), - '/directory': (context) => GetDirectoryPage(), + 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/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart index ae0d5ad4eefb..28afca065121 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 @@ -3,29 +3,35 @@ // found in the LICENSE file. import 'dart:io'; -import 'package:flutter/foundation.dart'; + import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Screen that shows an example of openFiles class OpenImagePage extends StatelessWidget { - void _openImageFile(BuildContext context) async { + /// Default Constructor + const OpenImagePage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + // #docregion SingleOpen final XTypeGroup typeGroup = XTypeGroup( label: 'images', - extensions: ['jpg', 'png'], + 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; - await showDialog( + await showDialog( context: context, - builder: (context) => ImageDisplay(fileName, filePath), + builder: (BuildContext context) => ImageDisplay(fileName, filePath), ); } @@ -33,7 +39,7 @@ class OpenImagePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open an image"), + title: const Text('Open an image'), ), body: Center( child: Column( @@ -41,10 +47,13 @@ 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: Text('Press to open an image file(png, jpg)'), + child: const Text('Press to open an image file(png, jpg)'), onPressed: () => _openImageFile(context), ), ], @@ -56,15 +65,16 @@ 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, {Key? key}) + : super(key: key); + /// Image's name final String fileName; /// Image's path final String filePath; - /// Default Constructor - ImageDisplay(this.fileName, this.filePath); - @override Widget build(BuildContext context) { return AlertDialog( @@ -72,7 +82,7 @@ class ImageDisplay extends StatelessWidget { // 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: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () { 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 5bf080eff450..22703425b47b 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 @@ -3,32 +3,38 @@ // found in the LICENSE file. import 'dart:io'; -import 'package:flutter/foundation.dart'; + import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// Screen that shows an example of openFiles class OpenMultipleImagesPage extends StatelessWidget { - void _openImageFile(BuildContext context) async { + /// Default Constructor + const OpenMultipleImagesPage({Key? key}) : super(key: key); + + Future _openImageFile(BuildContext context) async { + // #docregion MultiOpen final XTypeGroup jpgsTypeGroup = XTypeGroup( label: 'JPEGs', - extensions: ['jpg', 'jpeg'], + extensions: ['jpg', 'jpeg'], ); final XTypeGroup pngTypeGroup = XTypeGroup( label: 'PNGs', - extensions: ['png'], + extensions: ['png'], ); - final List files = await openFiles(acceptedTypeGroups: [ + final List files = await openFiles(acceptedTypeGroups: [ jpgsTypeGroup, pngTypeGroup, ]); + // #enddocregion MultiOpen if (files.isEmpty) { // Operation was canceled by the user. return; } - await showDialog( + await showDialog( context: context, - builder: (context) => MultipleImagesDisplay(files), + builder: (BuildContext context) => MultipleImagesDisplay(files), ); } @@ -36,7 +42,7 @@ class OpenMultipleImagesPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open multiple images"), + title: const Text('Open multiple images'), ), body: Center( child: Column( @@ -44,10 +50,13 @@ 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: Text('Press to open multiple images (png, jpg)'), + child: const Text('Press to open multiple images (png, jpg)'), onPressed: () => _openImageFile(context), ), ], @@ -59,23 +68,23 @@ class OpenMultipleImagesPage extends StatelessWidget { /// 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; - /// Default Constructor - MultipleImagesDisplay(this.files); - @override Widget build(BuildContext context) { return AlertDialog( - title: Text('Gallery'), + 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( - (file) => Flexible( + (XFile file) => Flexible( child: kIsWeb ? Image.network(file.path) : Image.file(File(file.path))), @@ -83,7 +92,7 @@ class MultipleImagesDisplay extends StatelessWidget { ], ), ), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () { 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 8451378f7baa..27fb0b749c3b 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,15 +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 { - void _openTextFile(BuildContext context) async { + /// Default Constructor + const OpenTextPage({Key? key}) : super(key: key); + + Future _openTextFile(BuildContext context) async { final XTypeGroup typeGroup = XTypeGroup( label: 'text', - extensions: ['txt', 'json'], + extensions: ['txt', 'json'], + ); + // 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, ); - final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); if (file == null) { // Operation was canceled by the user. return; @@ -20,9 +32,9 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( + await showDialog( context: context, - builder: (context) => TextDisplay(fileName, fileContent), + builder: (BuildContext context) => TextDisplay(fileName, fileContent), ); } @@ -30,7 +42,7 @@ class OpenTextPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Open a text file"), + title: const Text('Open a text file'), ), body: Center( child: Column( @@ -38,10 +50,13 @@ 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: Text('Press to open a text file (json, txt)'), + child: const Text('Press to open a text file (json, txt)'), onPressed: () => _openTextFile(context), ), ], @@ -53,15 +68,16 @@ 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, {Key? key}) + : super(key: key); + /// File's name final String fileName; /// File to display final String fileContent; - /// Default Constructor - TextDisplay(this.fileName, this.fileContent); - @override Widget build(BuildContext context) { return AlertDialog( @@ -71,7 +87,7 @@ class TextDisplay extends StatelessWidget { child: Text(fileContent), ), ), - actions: [ + actions: [ TextButton( child: const Text('Close'), onPressed: () => Navigator.pop(context), 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..c67c93fa63f2 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/readme_standalone_excerpts.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. + +// 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 + } +} 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 1610fc05164d..d2a8f30db06b 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 @@ -5,24 +5,38 @@ import 'dart:typed_data'; import 'package:file_selector/file_selector.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 TextEditingController _nameController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); - void _saveFile() async { - String? path = await getSavePath(); + Future _saveFile() async { + 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); - final String fileMimeType = 'text/plain'; + const String fileMimeType = 'text/plain'; final XFile textFile = XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); } @@ -30,7 +44,7 @@ class SaveTextPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Save text into a file"), + title: const Text('Save text into a file'), ), body: Center( child: Column( @@ -42,7 +56,7 @@ class SaveTextPage extends StatelessWidget { minLines: 1, maxLines: 12, controller: _nameController, - decoration: InputDecoration( + decoration: const InputDecoration( hintText: '(Optional) Suggest File Name', ), ), @@ -53,18 +67,21 @@ class SaveTextPage extends StatelessWidget { minLines: 1, maxLines: 12, controller: _contentController, - decoration: InputDecoration( + decoration: const InputDecoration( hintText: 'Enter File Contents', ), ), ), - SizedBox(height: 10), + 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: Text('Press to save a text file'), + child: const Text('Press to save a text file'), onPressed: () => _saveFile(), ), ], 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/connectivity/connectivity/example/macos/Podfile b/packages/file_selector/file_selector/example/macos/Podfile similarity index 100% rename from packages/connectivity/connectivity/example/macos/Podfile rename to packages/file_selector/file_selector/example/macos/Podfile 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/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/file_selector/file_selector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 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/connectivity/connectivity/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/file_selector/file_selector/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/file_selector/file_selector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/connectivity/connectivity/example/macos/Runner/AppDelegate.swift b/packages/file_selector/file_selector/example/macos/Runner/AppDelegate.swift similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/AppDelegate.swift rename to packages/file_selector/file_selector/example/macos/Runner/AppDelegate.swift diff --git a/packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/file_selector/file_selector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png 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/connectivity/connectivity/example/macos/Runner/Configs/Debug.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Configs/Debug.xcconfig rename to packages/file_selector/file_selector/example/macos/Runner/Configs/Debug.xcconfig diff --git a/packages/connectivity/connectivity/example/macos/Runner/Configs/Release.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Configs/Release.xcconfig rename to packages/file_selector/file_selector/example/macos/Runner/Configs/Release.xcconfig diff --git a/packages/connectivity/connectivity/example/macos/Runner/Configs/Warnings.xcconfig b/packages/file_selector/file_selector/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Configs/Warnings.xcconfig rename to packages/file_selector/file_selector/example/macos/Runner/Configs/Warnings.xcconfig 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/connectivity/connectivity/example/macos/Runner/Info.plist b/packages/file_selector/file_selector/example/macos/Runner/Info.plist similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Info.plist rename to packages/file_selector/file_selector/example/macos/Runner/Info.plist diff --git a/packages/connectivity/connectivity/example/macos/Runner/MainFlutterWindow.swift b/packages/file_selector/file_selector/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/MainFlutterWindow.swift rename to packages/file_selector/file_selector/example/macos/Runner/MainFlutterWindow.swift 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 4987c836ad63..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 @@ -8,9 +8,6 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - flutter: - sdk: flutter - file_selector: # When depending on this package from a real application you should use: # file_selector: ^x.y.z @@ -18,8 +15,12 @@ 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: ../ + 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/.gitignore b/packages/file_selector/file_selector/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/CMakeLists.txt b/packages/file_selector/file_selector/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..c0270746b1b9 --- /dev/null +++ b/packages/file_selector/file_selector/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/file_selector/file_selector/example/windows/flutter/CMakeLists.txt b/packages/file_selector/file_selector/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..930d2071a324 --- /dev/null +++ b/packages/file_selector/file_selector/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/file_selector/file_selector/example/windows/flutter/generated_plugins.cmake b/packages/file_selector/file_selector/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..a423a02476a2 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/CMakeLists.txt b/packages/file_selector/file_selector/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..b9e550fba8e1 --- /dev/null +++ b/packages/file_selector/file_selector/example/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/file_selector/file_selector/example/windows/runner/Runner.rc b/packages/file_selector/file_selector/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..5fdea291cf19 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/flutter_window.cpp b/packages/file_selector/file_selector/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8254bd9ff3c1 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/flutter_window.h b/packages/file_selector/file_selector/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/main.cpp b/packages/file_selector/file_selector/example/windows/runner/main.cpp new file mode 100644 index 000000000000..df379fa0be93 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/resource.h b/packages/file_selector/file_selector/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/resources/app_icon.ico b/packages/file_selector/file_selector/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/file_selector/file_selector/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/file_selector/file_selector/example/windows/runner/runner.exe.manifest b/packages/file_selector/file_selector/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/file_selector/file_selector/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector/example/windows/runner/utils.cpp b/packages/file_selector/file_selector/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..fb7e945a63b7 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/utils.h b/packages/file_selector/file_selector/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/win32_window.cpp b/packages/file_selector/file_selector/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..85aa3614e8ad --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/example/windows/runner/win32_window.h b/packages/file_selector/file_selector/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/file_selector/file_selector/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/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart index c2803d60c972..322ae6cda483 100644 --- a/packages/file_selector/file_selector/lib/file_selector.dart +++ b/packages/file_selector/file_selector/lib/file_selector.dart @@ -9,7 +9,24 @@ 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. +/// +/// [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 +38,24 @@ 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. +/// +/// [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 +67,25 @@ 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. +/// +/// [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 +99,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 e1725ef05b93..12a1fbc47782 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -1,22 +1,29 @@ name: file_selector -description: Flutter plugin for opening and saving files. -repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector +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.2 +version: 0.8.4+3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: platforms: + macos: + default_package: file_selector_macos web: default_package: file_selector_web + windows: + default_package: file_selector_windows dependencies: + file_selector_macos: ^0.8.2 file_selector_platform_interface: ^2.0.0 file_selector_web: ^0.8.1 + file_selector_windows: ^0.8.2 flutter: sdk: flutter 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 6d5e215eeb01..887ab64c3c0c 100644 --- a/packages/file_selector/file_selector/test/file_selector_test.dart +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -2,23 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:file_selector/file_selector.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:test/fake.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { late FakeFileSelector fakePlatformImplementation; - final initialDirectory = '/home/flutteruser'; - final confirmButtonText = 'Use this profile picture'; - final suggestedName = 'suggested_name'; - final acceptedTypeGroups = [ - XTypeGroup(label: 'documents', mimeTypes: [ + const String initialDirectory = '/home/flutteruser'; + const String confirmButtonText = 'Use this profile picture'; + const String suggestedName = 'suggested_name'; + final List acceptedTypeGroups = [ + XTypeGroup(label: 'documents', mimeTypes: [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessing', ]), - XTypeGroup(label: 'images', extensions: [ + XTypeGroup(label: 'images', extensions: [ 'jpg', 'png', ]), @@ -30,7 +29,7 @@ void main() { }); group('openFile', () { - final expectedFile = XFile('path'); + final XFile expectedFile = XFile('path'); test('works', () async { fakePlatformImplementation @@ -40,7 +39,7 @@ void main() { acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse([expectedFile]); - final file = await openFile( + final XFile? file = await openFile( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, @@ -52,7 +51,7 @@ void main() { test('works with no arguments', () async { fakePlatformImplementation.setFileResponse([expectedFile]); - final file = await openFile(); + final XFile? file = await openFile(); expect(file, expectedFile); }); @@ -62,7 +61,7 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setFileResponse([expectedFile]); - final file = await openFile(initialDirectory: initialDirectory); + final XFile? file = await openFile(initialDirectory: initialDirectory); expect(file, expectedFile); }); @@ -71,7 +70,7 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setFileResponse([expectedFile]); - final file = await openFile(confirmButtonText: confirmButtonText); + final XFile? file = await openFile(confirmButtonText: confirmButtonText); expect(file, expectedFile); }); @@ -80,13 +79,14 @@ void main() { ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse([expectedFile]); - final file = await openFile(acceptedTypeGroups: acceptedTypeGroups); + final XFile? file = + await openFile(acceptedTypeGroups: acceptedTypeGroups); expect(file, expectedFile); }); }); group('openFiles', () { - final expectedFiles = [XFile('path')]; + final List expectedFiles = [XFile('path')]; test('works', () async { fakePlatformImplementation @@ -96,19 +96,19 @@ void main() { acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse(expectedFiles); - final file = await openFiles( + final List files = await openFiles( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, ); - expect(file, expectedFiles); + expect(files, expectedFiles); }); test('works with no arguments', () async { fakePlatformImplementation.setFileResponse(expectedFiles); - final files = await openFiles(); + final List files = await openFiles(); expect(files, expectedFiles); }); @@ -118,7 +118,8 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setFileResponse(expectedFiles); - final files = await openFiles(initialDirectory: initialDirectory); + final List files = + await openFiles(initialDirectory: initialDirectory); expect(files, expectedFiles); }); @@ -127,7 +128,8 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setFileResponse(expectedFiles); - final files = await openFiles(confirmButtonText: confirmButtonText); + final List files = + await openFiles(confirmButtonText: confirmButtonText); expect(files, expectedFiles); }); @@ -136,13 +138,14 @@ void main() { ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) ..setFileResponse(expectedFiles); - final files = await openFiles(acceptedTypeGroups: acceptedTypeGroups); + final List files = + await openFiles(acceptedTypeGroups: acceptedTypeGroups); expect(files, expectedFiles); }); }); group('getSavePath', () { - final expectedSavePath = '/example/path'; + const String expectedSavePath = '/example/path'; test('works', () async { fakePlatformImplementation @@ -153,7 +156,7 @@ void main() { suggestedName: suggestedName) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath( + final String? savePath = await getSavePath( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, acceptedTypeGroups: acceptedTypeGroups, @@ -166,7 +169,7 @@ void main() { test('works with no arguments', () async { fakePlatformImplementation.setPathResponse(expectedSavePath); - final savePath = await getSavePath(); + final String? savePath = await getSavePath(); expect(savePath, expectedSavePath); }); @@ -175,7 +178,8 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath(initialDirectory: initialDirectory); + final String? savePath = + await getSavePath(initialDirectory: initialDirectory); expect(savePath, expectedSavePath); }); @@ -184,7 +188,8 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath(confirmButtonText: confirmButtonText); + final String? savePath = + await getSavePath(confirmButtonText: confirmButtonText); expect(savePath, expectedSavePath); }); @@ -193,7 +198,7 @@ void main() { ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) ..setPathResponse(expectedSavePath); - final savePath = + final String? savePath = await getSavePath(acceptedTypeGroups: acceptedTypeGroups); expect(savePath, expectedSavePath); }); @@ -203,13 +208,13 @@ void main() { ..setExpectations(suggestedName: suggestedName) ..setPathResponse(expectedSavePath); - final savePath = await getSavePath(suggestedName: suggestedName); + final String? savePath = await getSavePath(suggestedName: suggestedName); expect(savePath, expectedSavePath); }); }); group('getDirectoryPath', () { - final expectedDirectoryPath = '/example/path'; + const String expectedDirectoryPath = '/example/path'; test('works', () async { fakePlatformImplementation @@ -218,7 +223,7 @@ void main() { confirmButtonText: confirmButtonText) ..setPathResponse(expectedDirectoryPath); - final directoryPath = await getDirectoryPath( + final String? directoryPath = await getDirectoryPath( initialDirectory: initialDirectory, confirmButtonText: confirmButtonText, ); @@ -229,7 +234,7 @@ void main() { test('works with no arguments', () async { fakePlatformImplementation.setPathResponse(expectedDirectoryPath); - final directoryPath = await getDirectoryPath(); + final String? directoryPath = await getDirectoryPath(); expect(directoryPath, expectedDirectoryPath); }); @@ -238,7 +243,7 @@ void main() { ..setExpectations(initialDirectory: initialDirectory) ..setPathResponse(expectedDirectoryPath); - final directoryPath = + final String? directoryPath = await getDirectoryPath(initialDirectory: initialDirectory); expect(directoryPath, expectedDirectoryPath); }); @@ -248,7 +253,7 @@ void main() { ..setExpectations(confirmButtonText: confirmButtonText) ..setPathResponse(expectedDirectoryPath); - final directoryPath = + final String? directoryPath = await getDirectoryPath(confirmButtonText: confirmButtonText); expect(directoryPath, expectedDirectoryPath); }); @@ -279,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; } @@ -295,7 +302,7 @@ class FakeFileSelector extends Fake }) async { expect(acceptedTypeGroups, this.acceptedTypeGroups); expect(initialDirectory, this.initialDirectory); - expect(suggestedName, this.suggestedName); + expect(suggestedName, suggestedName); return files?[0]; } @@ -307,7 +314,7 @@ class FakeFileSelector extends Fake }) async { expect(acceptedTypeGroups, this.acceptedTypeGroups); expect(initialDirectory, this.initialDirectory); - expect(suggestedName, this.suggestedName); + expect(suggestedName, suggestedName); return files!; } 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..8a4f217f8b9a --- /dev/null +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -0,0 +1,40 @@ +## NEXT + +* 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/battery/battery_platform_interface/LICENSE b/packages/file_selector/file_selector_macos/LICENSE similarity index 100% rename from packages/battery/battery_platform_interface/LICENSE rename to packages/file_selector/file_selector_macos/LICENSE 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..3241b21d1e18 --- /dev/null +++ b/packages/file_selector/file_selector_macos/README.md @@ -0,0 +1,34 @@ +# file\_selector\_macos + +The macOS 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_macos`][2] in addition to +[depending on `file_selector`][3]. + +Once your pubspec includes the macOS implementation, you can use the +`file_selector` APIs normally. You should not use the `file_selector_macos` +APIs directly. + +### Entitlements + +You will need to [add an entitlement][4] 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://pub.dev/packages/file_selector_macos/install +[3]: https://pub.dev/packages/file_selector/install +[4]: 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..782fe679fcb0 --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/README.md @@ -0,0 +1,4 @@ +# `file_selector_macos` example + +Demonstrates macOS implementation of the +[`file_selector` plugin](https://pub.dev/packages/file_selector). 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..cbe268e1c7ab --- /dev/null +++ b/packages/file_selector/file_selector_macos/example/lib/main.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: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'; + +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..9e1d2074d5f2 --- /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 { + final 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..21da8c22fa4d --- /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 { + final XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + final 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..05c6d166fb6f --- /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 { + final 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/package_info/example/macos/.gitignore b/packages/file_selector/file_selector_macos/example/macos/.gitignore similarity index 100% rename from packages/package_info/example/macos/.gitignore rename to packages/file_selector/file_selector_macos/example/macos/.gitignore 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/package_info/example/macos/Podfile b/packages/file_selector/file_selector_macos/example/macos/Podfile similarity index 100% rename from packages/package_info/example/macos/Podfile rename to packages/file_selector/file_selector_macos/example/macos/Podfile 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/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/file_selector/file_selector_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 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/connectivity/connectivity/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/package_info/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/package_info/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/file_selector/file_selector_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift b/packages/file_selector/file_selector_macos/example/macos/Runner/AppDelegate.swift similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift rename to packages/file_selector/file_selector_macos/example/macos/Runner/AppDelegate.swift diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/packages/connectivity/connectivity_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 similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/file_selector/file_selector_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/packages/connectivity/connectivity/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/file_selector/file_selector_macos/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from packages/connectivity/connectivity/example/macos/Runner/Base.lproj/MainMenu.xib rename to packages/file_selector/file_selector_macos/example/macos/Runner/Base.lproj/MainMenu.xib 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/connectivity/connectivity_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Debug.xcconfig rename to packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Debug.xcconfig diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Release.xcconfig rename to packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Release.xcconfig diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Configs/Warnings.xcconfig rename to packages/file_selector/file_selector_macos/example/macos/Runner/Configs/Warnings.xcconfig 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/connectivity/connectivity_macos/example/macos/Runner/Info.plist b/packages/file_selector/file_selector_macos/example/macos/Runner/Info.plist similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/Info.plist rename to packages/file_selector/file_selector_macos/example/macos/Runner/Info.plist diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/file_selector/file_selector_macos/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift rename to packages/file_selector/file_selector_macos/example/macos/Runner/MainFlutterWindow.swift 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_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/Info.plist similarity index 100% rename from packages/google_sign_in/google_sign_in/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..dbe127282a17 --- /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.8.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.0.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..e50c296b005f --- /dev/null +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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.extensions?.isEmpty ?? true) && + (typeGroup.macUTIs?.isEmpty ?? true) && + (typeGroup.mimeTypes?.isEmpty ?? true)) { + return null; + } + 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..8c6d1f7c81ce --- /dev/null +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -0,0 +1,25 @@ +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.8.2+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: file_selector + platforms: + macos: + dartPluginClass: FileSelectorMacOS + pluginClass: FileSelectorPlugin + +dependencies: + cross_file: ^0.3.1 + file_selector_platform_interface: ^2.0.4 + flutter: + sdk: flutter + 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..1c1b9c11e069 --- /dev/null +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart @@ -0,0 +1,288 @@ +// Copyright 2013 The Flutter Authors. 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 { + 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: { + '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, + }), + ], + ); + }); + }); + 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: { + '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, + }), + ], + ); + }); + }); + + 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: { + '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', + }), + ], + ); + }); + 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: [ + XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ), + XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + ), + 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 ba595addfcaf..b1fa45b708d5 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,17 @@ +## 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`. + +## 2.0.3 + +* Minor code cleanup for new analysis rules. +* Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. + ## 2.0.2 * Update platform_plugin_interface version requirement. 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 34017acc90e0..c6d0f4a56155 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,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:cross_file/cross_file.dart'; -import 'package:flutter/services.dart'; - import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/file_selector'); @@ -27,8 +25,9 @@ class MethodChannelFileSelector extends FileSelectorPlatform { final List? path = await _channel.invokeListMethod( 'openFile', { - 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON()).toList(), + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': false, @@ -47,14 +46,15 @@ class MethodChannelFileSelector extends FileSelectorPlatform { final List? pathList = await _channel.invokeListMethod( 'openFile', { - 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON()).toList(), + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': true, }, ); - return pathList?.map((path) => XFile(path)).toList() ?? []; + return pathList?.map((String path) => XFile(path)).toList() ?? []; } /// Gets the path from a save dialog @@ -68,8 +68,9 @@ class MethodChannelFileSelector extends FileSelectorPlatform { return _channel.invokeMethod( 'getSavePath', { - 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON()).toList(), + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), 'initialDirectory': initialDirectory, 'suggestedName': suggestedName, 'confirmButtonText': confirmButtonText, 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 54a6557c4428..a23957af9110 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,7 +4,6 @@ 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'; @@ -33,7 +32,7 @@ abstract class FileSelectorPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [FileSelectorPlatform] when they register themselves. static set instance(FileSelectorPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } 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 3e3326379610..506dc1ce2de9 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 @@ -14,7 +14,7 @@ class XTypeGroup { this.mimeTypes, this.macUTIs, this.webWildCards, - }) : this.extensions = _removeLeadingDots(extensions); + }) : extensions = _removeLeadingDots(extensions); /// The 'name' or reference to this group of types final String? label; @@ -42,6 +42,15 @@ class XTypeGroup { }; } - static List? _removeLeadingDots(List? exts) => - exts?.map((ext) => ext.startsWith('.') ? ext.substring(1) : ext).toList(); + /// 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); + } + + 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/lib/src/web_helpers/web_helpers.dart b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart index 0f157ed0be5a..bc7136f80bd6 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart @@ -6,7 +6,7 @@ import 'dart:html'; /// Create anchor element with download attribute AnchorElement createAnchorElement(String href, String? suggestedName) { - final element = AnchorElement(href: href); + final AnchorElement element = AnchorElement(href: href); if (suggestedName == null) { element.download = 'download'; @@ -27,7 +27,7 @@ void addElementToContainerAndClick(Element container, Element element) { /// Initializes a DOM container where we can host elements. Element ensureInitialized(String id) { - var target = querySelector('#${id}'); + Element? target = querySelector('#$id'); if (target == null) { final Element targetElement = Element.tag('flt-x-file')..id = id; diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index ed0780537a80..4ba84782d436 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -1,22 +1,21 @@ name: file_selector_platform_interface description: A common platform interface for the file_selector plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_platform_interface 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.2 +version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: cross_file: ^0.3.0 flutter: sdk: flutter http: ^0.13.0 - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart index f5b609c93ed3..91e78b452961 100644 --- a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart @@ -2,16 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; - import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { + // Store the initial instance before any tests change it. + final FileSelectorPlatform initialInstance = FileSelectorPlatform.instance; + group('$FileSelectorPlatform', () { test('$MethodChannelFileSelector() is the default instance', () { - expect(FileSelectorPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Can be extended', () { 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 96a3c2d4f4c9..33f9fbf45a8b 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 @@ -2,17 +2,16 @@ // 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:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$MethodChannelFileSelector()', () { - MethodChannelFileSelector plugin = MethodChannelFileSelector(); + final MethodChannelFileSelector plugin = MethodChannelFileSelector(); final List log = []; @@ -27,27 +26,31 @@ void main() { group('#openFile', () { test('passes the accepted type groups correctly', () async { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], ); - final groupTwo = XTypeGroup( + final XTypeGroup groupTwo = XTypeGroup( label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); - await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); + await plugin + .openFile(acceptedTypeGroups: [group, groupTwo]); expect( log, [ isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()], + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], 'initialDirectory': null, 'confirmButtonText': null, 'multiple': false, @@ -56,14 +59,14 @@ void main() { ); }); test('passes initialDirectory correctly', () async { - await plugin.openFile(initialDirectory: "/example/directory"); + await plugin.openFile(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'confirmButtonText': null, 'multiple': false, }), @@ -71,7 +74,7 @@ void main() { ); }); test('passes confirmButtonText correctly', () async { - await plugin.openFile(confirmButtonText: "Open File"); + await plugin.openFile(confirmButtonText: 'Open File'); expect( log, @@ -79,7 +82,7 @@ void main() { isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, 'initialDirectory': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', 'multiple': false, }), ], @@ -88,27 +91,31 @@ void main() { }); group('#openFiles', () { test('passes the accepted type groups correctly', () async { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], ); - final groupTwo = XTypeGroup( + final XTypeGroup groupTwo = XTypeGroup( label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); - await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); + await plugin + .openFiles(acceptedTypeGroups: [group, groupTwo]); expect( log, [ isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()], + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], 'initialDirectory': null, 'confirmButtonText': null, 'multiple': true, @@ -117,14 +124,14 @@ void main() { ); }); test('passes initialDirectory correctly', () async { - await plugin.openFiles(initialDirectory: "/example/directory"); + await plugin.openFiles(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'confirmButtonText': null, 'multiple': true, }), @@ -132,7 +139,7 @@ void main() { ); }); test('passes confirmButtonText correctly', () async { - await plugin.openFiles(confirmButtonText: "Open File"); + await plugin.openFiles(confirmButtonText: 'Open File'); expect( log, @@ -140,7 +147,7 @@ void main() { isMethodCall('openFile', arguments: { 'acceptedTypeGroups': null, 'initialDirectory': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', 'multiple': true, }), ], @@ -150,27 +157,31 @@ void main() { group('#getSavePath', () { test('passes the accepted type groups correctly', () async { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'text', - extensions: ['txt'], - mimeTypes: ['text/plain'], - macUTIs: ['public.text'], + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], ); - final groupTwo = XTypeGroup( + final XTypeGroup groupTwo = XTypeGroup( label: 'image', - extensions: ['jpg'], - mimeTypes: ['image/jpg'], - macUTIs: ['public.image'], - webWildCards: ['image/*']); + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); - await plugin.getSavePath(acceptedTypeGroups: [group, groupTwo]); + await plugin + .getSavePath(acceptedTypeGroups: [group, groupTwo]); expect( log, [ isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()], + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], 'initialDirectory': null, 'suggestedName': null, 'confirmButtonText': null, @@ -179,14 +190,14 @@ void main() { ); }); test('passes initialDirectory correctly', () async { - await plugin.getSavePath(initialDirectory: "/example/directory"); + await plugin.getSavePath(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('getSavePath', arguments: { 'acceptedTypeGroups': null, - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'suggestedName': null, 'confirmButtonText': null, }), @@ -194,7 +205,7 @@ void main() { ); }); test('passes confirmButtonText correctly', () async { - await plugin.getSavePath(confirmButtonText: "Open File"); + await plugin.getSavePath(confirmButtonText: 'Open File'); expect( log, @@ -203,34 +214,34 @@ void main() { 'acceptedTypeGroups': null, 'initialDirectory': null, 'suggestedName': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', }), ], ); }); group('#getDirectoryPath', () { test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPath(initialDirectory: "/example/directory"); + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); expect( log, [ isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': "/example/directory", + 'initialDirectory': '/example/directory', 'confirmButtonText': null, }), ], ); }); test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: "Open File"); + await plugin.getDirectoryPath(confirmButtonText: 'Open File'); expect( log, [ isMethodCall('getDirectoryPath', arguments: { 'initialDirectory': null, - 'confirmButtonText': "Open File", + 'confirmButtonText': 'Open File', }), ], ); 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 e85a4929e411..e09605c109c5 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 @@ -2,19 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('XTypeGroup', () { test('toJSON() creates correct map', () { - final label = 'test group'; - final extensions = ['txt', 'jpg']; - final mimeTypes = ['text/plain']; - final macUTIs = ['public.plain-text']; - final 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 group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: label, extensions: extensions, mimeTypes: mimeTypes, @@ -22,7 +22,7 @@ void main() { webWildCards: webWildCards, ); - final jsonMap = group.toJSON(); + final Map jsonMap = group.toJSON(); expect(jsonMap['label'], label); expect(jsonMap['extensions'], extensions); expect(jsonMap['mimeTypes'], mimeTypes); @@ -31,22 +31,51 @@ void main() { }); test('A wildcard group can be created', () { - final group = XTypeGroup( + final XTypeGroup group = XTypeGroup( label: 'Any', ); - final jsonMap = group.toJSON(); + final Map jsonMap = group.toJSON(); expect(jsonMap['extensions'], null); 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', () { + final XTypeGroup group = XTypeGroup( + label: 'Any', + extensions: [], + mimeTypes: [], + macUTIs: [], + webWildCards: [], + ); + + expect(group.allowsAny, true); + }); + + test('allowsAny returns false if anything is set', () { + final XTypeGroup extensionOnly = + XTypeGroup(label: 'extensions', extensions: ['txt']); + final XTypeGroup mimeOnly = + XTypeGroup(label: 'mime', mimeTypes: ['text/plain']); + final XTypeGroup utiOnly = + XTypeGroup(label: 'utis', macUTIs: ['public.text']); + final XTypeGroup webOnly = + XTypeGroup(label: 'web', webWildCards: ['.txt']); + + expect(extensionOnly.allowsAny, false); + expect(mimeOnly.allowsAny, false); + expect(utiOnly.allowsAny, false); + expect(webOnly.allowsAny, false); }); test('Leading dots are removed from extensions', () { - final extensions = ['.txt', '.jpg']; - final group = XTypeGroup(extensions: extensions); + final List extensions = ['.txt', '.jpg']; + final XTypeGroup group = XTypeGroup(extensions: extensions); - expect(group.extensions, ['txt', 'jpg']); + 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 e2a863643027..3963601e2ac5 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,17 @@ +## 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. +* Removes dependency on `meta`. + ## 0.8.1+2 * Add `implements` to pubspec. diff --git a/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart index f000574861ab..ee1af8cb62fd 100644 --- a/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart +++ b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart @@ -3,10 +3,11 @@ // found in the LICENSE file. import 'dart:html'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:file_selector_web/src/dom_helper.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; void main() { group('dom_helper', () { @@ -15,7 +16,7 @@ void main() { late FileUploadInputElement input; FileList? createFileList(List files) { - final dataTransfer = DataTransfer(); + final DataTransfer dataTransfer = DataTransfer(); files.forEach(dataTransfer.items!.add); return dataTransfer.files as FileList?; } @@ -31,15 +32,15 @@ void main() { }); group('getFiles', () { - final mockFile1 = File(['123456'], 'file1.txt'); - final mockFile2 = File([], 'file2.txt'); + final File mockFile1 = File(['123456'], 'file1.txt'); + final File mockFile2 = File([], 'file2.txt'); testWidgets('works', (_) async { final Future> futureFiles = domHelper.getFiles( input: input, ); - setFilesAndTriggerChange([mockFile1, mockFile2]); + setFilesAndTriggerChange([mockFile1, mockFile2]); final List files = await futureFiles; @@ -62,7 +63,7 @@ void main() { // It should work the first time futureFiles = domHelper.getFiles(input: input); - setFilesAndTriggerChange([mockFile1]); + setFilesAndTriggerChange([mockFile1]); files = await futureFiles; @@ -71,7 +72,7 @@ void main() { // The same input should work more than once futureFiles = domHelper.getFiles(input: input); - setFilesAndTriggerChange([mockFile2]); + setFilesAndTriggerChange([mockFile2]); files = await futureFiles; @@ -80,14 +81,14 @@ void main() { }); testWidgets('sets the attributes and clicks it', (_) async { - final accept = '.jpg,.png'; - final multiple = true; + const String accept = '.jpg,.png'; + const bool multiple = true; bool wasClicked = false; //ignore: unawaited_futures input.onClick.first.then((_) => wasClicked = true); - final futureFile = domHelper.getFiles( + final Future> futureFile = domHelper.getFiles( accept: accept, multiple: multiple, input: input, @@ -103,7 +104,7 @@ void main() { 'The should be clicked otherwise no dialog will be shown', ); - setFilesAndTriggerChange([]); + setFilesAndTriggerChange([]); await futureFile; // It should be already removed from the DOM after the file is resolved. 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 c16aa1cf454e..43c88a2a4241 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 @@ -4,11 +4,12 @@ import 'dart:html'; import 'dart:typed_data'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_web/file_selector_web.dart'; import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { group('FileSelectorWeb', () { @@ -16,23 +17,25 @@ void main() { group('openFile', () { testWidgets('works', (WidgetTester _) async { - final mockFile = createXFile('1001', 'identity.png'); + final XFile mockFile = createXFile('1001', 'identity.png'); - final mockDomHelper = MockDomHelper() - ..setFiles([mockFile]) - ..expectAccept('.jpg,.jpeg,image/png,image/*') - ..expectMultiple(false); + final MockDomHelper mockDomHelper = MockDomHelper( + files: [mockFile], + expectAccept: '.jpg,.jpeg,image/png,image/*', + expectMultiple: false); - final plugin = FileSelectorWeb(domHelper: mockDomHelper); + final FileSelectorWeb plugin = + FileSelectorWeb(domHelper: mockDomHelper); - final typeGroup = XTypeGroup( + final XTypeGroup typeGroup = XTypeGroup( label: 'images', - extensions: ['jpg', 'jpeg'], - mimeTypes: ['image/png'], - webWildCards: ['image/*'], + extensions: ['jpg', 'jpeg'], + mimeTypes: ['image/png'], + webWildCards: ['image/*'], ); - final file = await plugin.openFile(acceptedTypeGroups: [typeGroup]); + final XFile file = + await plugin.openFile(acceptedTypeGroups: [typeGroup]); expect(file.name, mockFile.name); expect(await file.length(), 4); @@ -43,22 +46,24 @@ void main() { group('openFiles', () { testWidgets('works', (WidgetTester _) async { - final mockFile1 = createXFile('123456', 'file1.txt'); - final mockFile2 = createXFile('', 'file2.txt'); + final XFile mockFile1 = createXFile('123456', 'file1.txt'); + final XFile mockFile2 = createXFile('', 'file2.txt'); - final mockDomHelper = MockDomHelper() - ..setFiles([mockFile1, mockFile2]) - ..expectAccept('.txt') - ..expectMultiple(true); + final MockDomHelper mockDomHelper = MockDomHelper( + files: [mockFile1, mockFile2], + expectAccept: '.txt', + expectMultiple: true); - final plugin = FileSelectorWeb(domHelper: mockDomHelper); + final FileSelectorWeb plugin = + FileSelectorWeb(domHelper: mockDomHelper); - final typeGroup = XTypeGroup( + final XTypeGroup typeGroup = XTypeGroup( label: 'files', - extensions: ['.txt'], + extensions: ['.txt'], ); - final files = await plugin.openFiles(acceptedTypeGroups: [typeGroup]); + final List files = + await plugin.openFiles(acceptedTypeGroups: [typeGroup]); expect(files.length, 2); @@ -76,8 +81,8 @@ void main() { group('getSavePath', () { testWidgets('returns non-null', (WidgetTester _) async { - final plugin = FileSelectorWeb(); - final savePath = plugin.getSavePath(); + final FileSelectorWeb plugin = FileSelectorWeb(); + final Future savePath = plugin.getSavePath(); expect(await savePath, isNotNull); }); }); @@ -85,9 +90,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({ @@ -99,23 +112,11 @@ class MockDomHelper implements DomHelper { reason: 'Expected "accept" value does not match.'); expect(multiple, _expectedMultiple, reason: 'Expected "multiple" value does not match.'); - return Future.value(_files); - } - - void setFiles(List files) { - _files = files; - } - - void expectAccept(String accept) { - _expectedAccept = accept; - } - - void expectMultiple(bool multiple) { - _expectedMultiple = multiple; + return Future>.value(_files); } } XFile createXFile(String content, String name) { - final data = Uint8List.fromList(content.codeUnits); + final Uint8List data = Uint8List.fromList(content.codeUnits); return XFile.fromData(data, name: name, lastModified: DateTime.now()); } 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 e1a38dcdcd46..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,19 +5,22 @@ 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 { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index dd98c28d1a99..d8b93ee816f3 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -3,14 +3,13 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter dev_dependencies: - build_runner: ^1.10.0 file_selector_web: path: ../ flutter_driver: 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 f7c10b36a186..915a2a806496 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 @@ -3,16 +3,23 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_web/src/dom_helper.dart'; import 'package:file_selector_web/src/utils.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// The web implementation of [FileSelectorPlatform]. /// /// This class implements the `package:file_selector` functionality for the web. class FileSelectorWeb extends FileSelectorPlatform { + /// Default constructor, initializes _domHelper that we can use + /// to interact with the DOM. + /// overrides parameter allows for testing to override functions + FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) + : _domHelper = domHelper ?? DomHelper(); + final DomHelper _domHelper; /// Registers this class as the default instance of [FileSelectorPlatform]. @@ -20,19 +27,14 @@ class FileSelectorWeb extends FileSelectorPlatform { FileSelectorPlatform.instance = FileSelectorWeb(); } - /// Default constructor, initializes _domHelper that we can use - /// to interact with the DOM. - /// overrides parameter allows for testing to override functions - FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) - : _domHelper = domHelper ?? DomHelper(); - @override Future openFile({ List? acceptedTypeGroups, String? initialDirectory, String? confirmButtonText, }) async { - final files = await _openFiles(acceptedTypeGroups: acceptedTypeGroups); + final List files = + await _openFiles(acceptedTypeGroups: acceptedTypeGroups); return files.first; } @@ -68,7 +70,7 @@ class FileSelectorWeb extends FileSelectorPlatform { List? acceptedTypeGroups, bool multiple = false, }) async { - final accept = acceptedTypesToString(acceptedTypeGroups); + final String accept = acceptedTypesToString(acceptedTypeGroups); return _domHelper.getFiles( accept: accept, multiple: multiple, diff --git a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart index 0d251af9cc7f..1c3442f8dab5 100644 --- a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart +++ b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart @@ -4,27 +4,28 @@ import 'dart:async'; import 'dart:html'; -import 'package:meta/meta.dart'; -import 'package:flutter/services.dart'; + import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; /// Class to manipulate the DOM with the intention of reading files from it. class DomHelper { - final _container = Element.tag('file-selector'); - /// Default constructor, initializes the container DOM element. DomHelper() { - final body = querySelector('body')!; + final Element body = querySelector('body')!; body.children.add(_container); } + final Element _container = Element.tag('file-selector'); + /// Sets the attributes and waits for a file to be selected. Future> getFiles({ String accept = '', bool multiple = false, @visibleForTesting FileUploadInputElement? input, }) { - final Completer> completer = Completer(); + final Completer> completer = Completer>(); final FileUploadInputElement inputElement = input ?? FileUploadInputElement(); @@ -41,9 +42,9 @@ class DomHelper { completer.complete(files); }); - inputElement.onError.first.then((event) { + inputElement.onError.first.then((Event event) { final ErrorEvent error = event as ErrorEvent; - final platformException = PlatformException( + final PlatformException platformException = PlatformException( code: error.type, message: error.message, ); 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 e52c00d1c223..fe8d1f433647 100644 --- a/packages/file_selector/file_selector_web/lib/src/utils.dart +++ b/packages/file_selector/file_selector_web/lib/src/utils.dart @@ -6,9 +6,11 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac /// Convert list of XTypeGroups to a comma-separated string String acceptedTypesToString(List? acceptedTypes) { - if (acceptedTypes == null) return ''; - final List allTypes = []; - for (final group in acceptedTypes) { + if (acceptedTypes == null) { + return ''; + } + final List allTypes = []; + for (final XTypeGroup group in acceptedTypes) { _assertTypeGroupIsValid(group); if (group.extensions != null) { allTypes.addAll(group.extensions!.map(_normalizeExtension)); @@ -34,5 +36,5 @@ void _assertTypeGroupIsValid(XTypeGroup group) { /// 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 bbad45bf2d6b..c685cca9e884 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -1,12 +1,12 @@ name: file_selector_web description: Web platform implementation of file_selector -repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_web +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+2 +version: 0.8.1+5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -22,7 +22,6 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.3.0 dev_dependencies: flutter_test: 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 2951af24dff9..9bddfd2e6304 100644 --- a/packages/file_selector/file_selector_web/test/utils_test.dart +++ b/packages/file_selector/file_selector_web/test/utils_test.dart @@ -2,54 +2,55 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; -import 'package:file_selector_web/src/utils.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('FileSelectorWeb utils', () { group('acceptedTypesToString', () { test('works', () { - final List acceptedTypes = [ - XTypeGroup(label: 'images', webWildCards: ['images/*']), - XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), - XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['images/*']), + XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'images/*,.jpg,.jpeg,image/png'); }); test('works with an empty list', () { - final List acceptedTypes = []; - final accepts = acceptedTypesToString(acceptedTypes); + final List acceptedTypes = []; + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, ''); }); test('works with extensions', () { - final List acceptedTypes = [ - XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), - XTypeGroup(label: 'pngs', extensions: ['png']), + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), + XTypeGroup(label: 'pngs', extensions: ['png']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, '.jpeg,.jpg,.png'); }); test('works with mime types', () { - final List acceptedTypes = [ - XTypeGroup(label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), - XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + final List acceptedTypes = [ + XTypeGroup( + label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'image/jpeg,image/jpg,image/png'); }); test('works with web wild cards', () { - final List acceptedTypes = [ - XTypeGroup(label: 'images', webWildCards: ['image/*']), - XTypeGroup(label: 'audios', webWildCards: ['audio/*']), - XTypeGroup(label: 'videos', webWildCards: ['video/*']), + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['image/*']), + XTypeGroup(label: 'audios', webWildCards: ['audio/*']), + XTypeGroup(label: 'videos', webWildCards: ['video/*']), ]; - final accepts = acceptedTypesToString(acceptedTypes); + final String accepts = acceptedTypesToString(acceptedTypes); expect(accepts, 'image/*,audio/*,video/*'); }); }); diff --git a/packages/file_selector/file_selector_windows/.gitignore b/packages/file_selector/file_selector_windows/.gitignore new file mode 100644 index 000000000000..0393a47ff732 --- /dev/null +++ b/packages/file_selector/file_selector_windows/.gitignore @@ -0,0 +1,5 @@ +.dart_tool +.packages +.flutter-plugins +.flutter-plugins-dependencies +pubspec.lock diff --git a/packages/file_selector/file_selector_windows/.metadata b/packages/file_selector/file_selector_windows/.metadata new file mode 100644 index 000000000000..720a4596c087 --- /dev/null +++ b/packages/file_selector/file_selector_windows/.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_windows/AUTHORS b/packages/file_selector/file_selector_windows/AUTHORS new file mode 100644 index 000000000000..557dff97933b --- /dev/null +++ b/packages/file_selector/file_selector_windows/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_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md new file mode 100644 index 000000000000..f5354b3286bc --- /dev/null +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -0,0 +1,30 @@ +## NEXT + +* 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. +* Switches to an internal method channel implementation. + +## 0.0.2+1 + +* Update README + +## 0.0.2 + +* Update SDK constraint to signal compatibility with null safety. + +## 0.0.1 + +* Initial Windows implementation of `file_selector`. diff --git a/packages/connectivity/connectivity/LICENSE b/packages/file_selector/file_selector_windows/LICENSE similarity index 100% rename from packages/connectivity/connectivity/LICENSE rename to packages/file_selector/file_selector_windows/LICENSE diff --git a/packages/file_selector/file_selector_windows/README.md b/packages/file_selector/file_selector_windows/README.md new file mode 100644 index 000000000000..69fb088d599e --- /dev/null +++ b/packages/file_selector/file_selector_windows/README.md @@ -0,0 +1,19 @@ +# file\_selector\_windows + +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. + +[1]: https://pub.dev/packages/file_selector +[2]: https://pub.dev/packages/file_selector_windows/install +[3]: https://pub.dev/packages/file_selector/install diff --git a/packages/file_selector/file_selector_windows/example/.gitignore b/packages/file_selector/file_selector_windows/example/.gitignore new file mode 100644 index 000000000000..7abd0753cfc3 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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_windows/example/.metadata b/packages/file_selector/file_selector_windows/example/.metadata new file mode 100644 index 000000000000..897381f2373f --- /dev/null +++ b/packages/file_selector/file_selector_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: 7736f3bc90270dcb0480db2ccffbf1d13c28db85 + channel: dev + +project_type: app diff --git a/packages/file_selector/file_selector_windows/example/README.md b/packages/file_selector/file_selector_windows/example/README.md new file mode 100644 index 000000000000..c8a3cce44a9a --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/README.md @@ -0,0 +1,4 @@ +# `file_selector_windows` Example + +Demonstrates Windows implementation of the +[`file_selector` plugin](https://pub.dev/packages/file_selector). 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 new file mode 100644 index 000000000000..0699dd121541 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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_windows/example/lib/home_page.dart b/packages/file_selector/file_selector_windows/example/lib/home_page.dart new file mode 100644 index 000000000000..a4b2ae1f63ea --- /dev/null +++ b/packages/file_selector/file_selector_windows/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_windows/example/lib/main.dart b/packages/file_selector/file_selector_windows/example/lib/main.dart new file mode 100644 index 000000000000..cbe268e1c7ab --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/lib/main.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: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'; + +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_windows/example/lib/open_image_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart new file mode 100644 index 000000000000..9e1d2074d5f2 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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 { + final 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_windows/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart new file mode 100644 index 000000000000..21da8c22fa4d --- /dev/null +++ b/packages/file_selector/file_selector_windows/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 { + final XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + final 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_windows/example/lib/open_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart new file mode 100644 index 000000000000..05c6d166fb6f --- /dev/null +++ b/packages/file_selector/file_selector_windows/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 { + final 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_windows/example/lib/save_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart new file mode 100644 index 000000000000..9803f285a536 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml new file mode 100644 index 000000000000..a3e69a6186f8 --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: example +description: Example for file_selector_windows implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + file_selector_platform_interface: ^2.0.0 + file_selector_windows: + # When depending on this package from a real application you should use: + # file_selector_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: .. + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/file_selector/file_selector_windows/example/windows/.gitignore b/packages/file_selector/file_selector_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_windows/example/windows/CMakeLists.txt b/packages/file_selector/file_selector_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..57d4c0c59d30 --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/windows/CMakeLists.txt @@ -0,0 +1,100 @@ +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") + +# Enable the test target +set(include_file_selector_windows_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS file_selector_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/file_selector/file_selector_windows/example/windows/flutter/CMakeLists.txt b/packages/file_selector/file_selector_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..b2e4bd8d658b --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake b/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..63eda9b7b59f --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +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) diff --git a/packages/file_selector/file_selector_windows/example/windows/runner/CMakeLists.txt b/packages/file_selector/file_selector_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..de2d8916b72b --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_windows/example/windows/runner/Runner.rc b/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..51812dcd4878 --- /dev/null +++ b/packages/file_selector/file_selector_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", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2021 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/file_selector/file_selector_windows/example/windows/runner/flutter_window.cpp b/packages/file_selector/file_selector_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..217bf9b69e67 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/example/windows/runner/flutter_window.h b/packages/file_selector/file_selector_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..7cbf3d3ebbb2 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/example/windows/runner/main.cpp b/packages/file_selector/file_selector_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..1285aabf714a --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/example/windows/runner/resource.h b/packages/file_selector/file_selector_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_windows/example/windows/runner/resources/app_icon.ico b/packages/file_selector/file_selector_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/file_selector/file_selector_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/file_selector/file_selector_windows/example/windows/runner/runner.exe.manifest b/packages/file_selector/file_selector_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/file_selector/file_selector_windows/example/windows/runner/utils.cpp b/packages/file_selector/file_selector_windows/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..8b8eaa54539a --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/example/windows/runner/utils.h b/packages/file_selector/file_selector_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..6d1cc48f0426 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/example/windows/runner/win32_window.cpp b/packages/file_selector/file_selector_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..34738de2d35b --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/example/windows/runner/win32_window.h b/packages/file_selector/file_selector_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..0f8bd1b7f920 --- /dev/null +++ b/packages/file_selector/file_selector_windows/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/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart new file mode 100644 index 000000000000..b91a22355572 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.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: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'); + +/// 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; + + /// Registers the Windows implementation. + static void registerWith() { + FileSelectorPlatform.instance = FileSelectorWindows(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + 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); + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + 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() ?? []; + } + + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + 'getSavePath', + { + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), + 'initialDirectory': initialDirectory, + 'suggestedName': suggestedName, + 'confirmButtonText': confirmButtonText, + }, + ); + } + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + 'getDirectoryPath', + { + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + }, + ); + } +} diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml new file mode 100644 index 000000000000..7c933b2778d8 --- /dev/null +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -0,0 +1,25 @@ +name: file_selector_windows +description: Windows implementation of the file_selector plugin. +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+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: file_selector + platforms: + windows: + dartPluginClass: FileSelectorWindows + pluginClass: FileSelectorWindows + +dependencies: + cross_file: ^0.3.1 + file_selector_platform_interface: ^2.0.4 + flutter: + sdk: flutter + flutter_test: + sdk: flutter 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 new file mode 100644 index 000000000000..72604dd1668c --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart @@ -0,0 +1,257 @@ +// Copyright 2013 The Flutter Authors. 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:file_selector_windows/file_selector_windows.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$FileSelectorWindows()', () { + final FileSelectorWindows plugin = FileSelectorWindows(); + + final List log = []; + + setUp(() { + plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return null; + }); + + log.clear(); + }); + + test('registered instance', () { + FileSelectorWindows.registerWith(); + expect(FileSelectorPlatform.instance, isA()); + }); + + 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, + }), + ], + ); + }); + }); + 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, + }), + ], + ); + }); + }); + + 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', + }), + ], + ); + }); + }); + }); + }); +} diff --git a/packages/file_selector/file_selector_windows/windows/.gitignore b/packages/file_selector/file_selector_windows/windows/.gitignore new file mode 100644 index 000000000000..b3eb2be169a5 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# 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/file_selector/file_selector_windows/windows/CMakeLists.txt b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt new file mode 100644 index 000000000000..01c9f58b98a2 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.14) +set(PROJECT_NAME "file_selector_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +list(APPEND PLUGIN_SOURCES + "file_dialog_controller.cpp" + "file_dialog_controller.h" + "file_selector_plugin.cpp" + "file_selector_plugin.h" + "string_utils.cpp" + "string_utils.h" +) + +add_library(${PLUGIN_NAME} SHARED + "file_selector_windows.cpp" + "include/file_selector_windows/file_selector_windows.h" + ${PLUGIN_SOURCES} +) +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 flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin +set(file_selector_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. +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 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/file_selector_plugin_test.cpp + test/test_main.cpp + test/test_file_dialog_controller.cpp + test/test_file_dialog_controller.h + test/test_utils.cpp + test/test_utils.h + ${PLUGIN_SOURCES} +) +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) +# 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/file_selector/file_selector_windows/windows/file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp new file mode 100644 index 000000000000..e4b1a2afcdde --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.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 "file_dialog_controller.h" + +#include +#include +#include + +_COM_SMARTPTR_TYPEDEF(IFileOpenDialog, IID_IFileOpenDialog); + +namespace file_selector_windows { + +FileDialogController::FileDialogController(IFileDialog* dialog) + : dialog_(dialog) {} + +FileDialogController::~FileDialogController() {} + +HRESULT FileDialogController::SetDefaultFolder(IShellItem* folder) { + return dialog_->SetDefaultFolder(folder); +} + +HRESULT FileDialogController::SetFileName(const wchar_t* name) { + return dialog_->SetFileName(name); +} + +HRESULT FileDialogController::SetFileTypes(UINT count, + COMDLG_FILTERSPEC* filters) { + return dialog_->SetFileTypes(count, filters); +} + +HRESULT FileDialogController::SetOkButtonLabel(const wchar_t* text) { + return dialog_->SetOkButtonLabel(text); +} + +HRESULT FileDialogController::GetOptions( + FILEOPENDIALOGOPTIONS* out_options) const { + return dialog_->GetOptions(out_options); +} + +HRESULT FileDialogController::SetOptions(FILEOPENDIALOGOPTIONS options) { + return dialog_->SetOptions(options); +} + +HRESULT FileDialogController::Show(HWND parent) { + return dialog_->Show(parent); +} + +HRESULT FileDialogController::GetResult(IShellItem** out_item) const { + return dialog_->GetResult(out_item); +} + +HRESULT FileDialogController::GetResults(IShellItemArray** out_items) const { + IFileOpenDialogPtr open_dialog; + HRESULT result = dialog_->QueryInterface(IID_PPV_ARGS(&open_dialog)); + if (!SUCCEEDED(result)) { + return result; + } + result = open_dialog->GetResults(out_items); + return result; +} + +FileDialogControllerFactory::~FileDialogControllerFactory() {} + +} // namespace file_selector_windows 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 new file mode 100644 index 000000000000..e7357338243e --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h @@ -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. +#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_DIALOG_CONTROLLER_H_ +#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_DIALOG_CONTROLLER_H_ + +#include +#include +#include +#include + +#include + +_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog); + +namespace file_selector_windows { + +// A thin wrapper for IFileDialog to allow for faking and inspection in tests. +// +// Since this class defines the end of what can be unit tested, it should +// contain as little logic as possible. +class FileDialogController { + public: + // Creates a controller managing |dialog|. + FileDialogController(IFileDialog* dialog); + virtual ~FileDialogController(); + + // Disallow copy and assign. + FileDialogController(const FileDialogController&) = delete; + FileDialogController& operator=(const FileDialogController&) = delete; + + // IFileDialog wrappers: + virtual HRESULT SetDefaultFolder(IShellItem* folder); + virtual HRESULT SetFileName(const wchar_t* name); + virtual HRESULT SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters); + virtual HRESULT SetOkButtonLabel(const wchar_t* text); + virtual HRESULT GetOptions(FILEOPENDIALOGOPTIONS* out_options) const; + virtual HRESULT SetOptions(FILEOPENDIALOGOPTIONS options); + virtual HRESULT Show(HWND parent); + virtual HRESULT GetResult(IShellItem** out_item) const; + + // IFileOpenDialog wrapper. This will fail if the IFileDialog* provided to the + // constructor was not an IFileOpenDialog instance. + virtual HRESULT GetResults(IShellItemArray** out_items) const; + + private: + IFileDialogPtr dialog_ = nullptr; +}; + +// Interface for creating FileDialogControllers, to allow for dependency +// injection. +class FileDialogControllerFactory { + public: + virtual ~FileDialogControllerFactory(); + + virtual std::unique_ptr CreateController( + IFileDialog* dialog) const = 0; +}; + +} // namespace file_selector_windows + +#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_DIALOG_CONTROLLER_H_ 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 new file mode 100644 index 000000000000..870bc281b6f6 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp @@ -0,0 +1,362 @@ +// Copyright 2013 The Flutter 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 "file_selector_plugin.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "file_dialog_controller.h" +#include "string_utils.h" + +_COM_SMARTPTR_TYPEDEF(IEnumShellItems, IID_IEnumShellItems); +_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog); +_COM_SMARTPTR_TYPEDEF(IShellItem, IID_IShellItem); +_COM_SMARTPTR_TYPEDEF(IShellItemArray, IID_IShellItemArray); + +namespace file_selector_windows { + +namespace { + +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); +} + +// Returns the path for |shell_item| as a UTF-8 string, or an +// empty string on failure. +std::string GetPathForShellItem(IShellItem* shell_item) { + if (shell_item == nullptr) { + return ""; + } + wchar_t* wide_path = nullptr; + if (!SUCCEEDED(shell_item->GetDisplayName(SIGDN_FILESYSPATH, &wide_path))) { + return ""; + } + std::string path = Utf8FromUtf16(wide_path); + ::CoTaskMemFree(wide_path); + return path; +} + +// Implementation of FileDialogControllerFactory that makes standard +// FileDialogController instances. +class DefaultFileDialogControllerFactory : public FileDialogControllerFactory { + public: + DefaultFileDialogControllerFactory() {} + virtual ~DefaultFileDialogControllerFactory() {} + + // Disallow copy and assign. + DefaultFileDialogControllerFactory( + const DefaultFileDialogControllerFactory&) = delete; + DefaultFileDialogControllerFactory& operator=( + const DefaultFileDialogControllerFactory&) = delete; + + std::unique_ptr CreateController( + IFileDialog* dialog) const override { + assert(dialog != nullptr); + return std::make_unique(dialog); + } +}; + +// Wraps an IFileDialog, managing object lifetime as a scoped object and +// providing a simplified API for interacting with it as needed for the plugin. +class DialogWrapper { + public: + explicit DialogWrapper(const FileDialogControllerFactory& dialog_factory, + IID type) { + is_open_dialog_ = type == CLSID_FileOpenDialog; + IFileDialogPtr dialog = nullptr; + last_result_ = CoCreateInstance(type, nullptr, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&dialog)); + dialog_controller_ = dialog_factory.CreateController(dialog); + } + + // Attempts to set the default folder for the dialog to |path|, + // if it exists. + void SetDefaultFolder(const std::string& path) { + std::wstring wide_path = Utf16FromUtf8(path); + IShellItemPtr item; + last_result_ = SHCreateItemFromParsingName(wide_path.c_str(), nullptr, + IID_PPV_ARGS(&item)); + if (!SUCCEEDED(last_result_)) { + return; + } + dialog_controller_->SetDefaultFolder(item); + } + + // Sets the file name that is initially shown in the dialog. + void SetFileName(const std::string& 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) { + std::wstring wide_label = Utf16FromUtf8(label); + last_result_ = dialog_controller_->SetOkButtonLabel(wide_label.c_str()); + } + + // Adds the given options to the dialog's current option set. + void AddOptions(FILEOPENDIALOGOPTIONS new_options) { + FILEOPENDIALOGOPTIONS options; + last_result_ = dialog_controller_->GetOptions(&options); + if (!SUCCEEDED(last_result_)) { + return; + } + options |= new_options; + if (options & FOS_PICKFOLDERS) { + opening_directory_ = true; + } + last_result_ = dialog_controller_->SetOptions(options); + } + + // Sets the filters for allowed file types to select. + void SetFileTypeFilters(const EncodableList& filters) { + const std::wstring spec_delimiter = L";"; + const std::wstring file_wildcard = L"*."; + std::vector filter_specs; + // Temporary ownership of the constructed strings whose data is used in + // filter_specs, so that they live until the call to SetFileTypes is done. + std::vector filter_names; + std::vector filter_extensions; + filter_extensions.reserve(filters.size()); + 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""); + filter_extensions.push_back(L""); + std::wstring& spec = filter_extensions.back(); + if (!extensions || extensions->empty()) { + spec += L"*.*"; + } else { + for (const EncodableValue& extension : *extensions) { + if (!spec.empty()) { + spec += spec_delimiter; + } + spec += + file_wildcard + Utf16FromUtf8(std::get(extension)); + } + } + filter_specs.push_back({filter_names.back().c_str(), spec.c_str()}); + } + last_result_ = dialog_controller_->SetFileTypes( + 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) { + assert(dialog_controller_); + last_result_ = dialog_controller_->Show(parent_window); + if (!SUCCEEDED(last_result_)) { + return EncodableValue(); + } + + if (is_open_dialog_) { + IShellItemArrayPtr shell_items; + last_result_ = dialog_controller_->GetResults(&shell_items); + if (!SUCCEEDED(last_result_)) { + return EncodableValue(); + } + IEnumShellItemsPtr item_enumerator; + last_result_ = shell_items->EnumItems(&item_enumerator); + if (!SUCCEEDED(last_result_)) { + return EncodableValue(); + } + 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(); + } + EncodableValue file(GetPathForShellItem(shell_item)); + return file; + } + } + + // Returns the result of the last Win32 API call related to this object. + HRESULT last_result() { return last_result_; } + + private: + // The dialog controller that all interactions are mediated through, to allow + // for unit testing. + std::unique_ptr dialog_controller_; + bool is_open_dialog_; + bool opening_directory_ = false; + 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; + DialogWrapper dialog(dialog_factory, dialog_type); + if (!SUCCEEDED(dialog.last_result())) { + result->Error("System error", "Could not create dialog", + EncodableValue(dialog.last_result())); + return; + } + + FILEOPENDIALOGOPTIONS dialog_options = 0; + if (method.compare(kGetDirectoryPathMethod) == 0) { + dialog_options |= FOS_PICKFOLDERS; + } + const auto* allow_multiple_selection = + std::get_if(ValueOrNull(args, kMultipleKey)); + if (allow_multiple_selection && *allow_multiple_selection) { + 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); + } + 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); + } + + 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())); + } + result->Success(files); +} + +// Returns the top-level window that owns |view|. +HWND GetRootWindow(flutter::FlutterView* view) { + return ::GetAncestor(view->GetNativeWindow(), GA_ROOT); +} + +} // namespace + +// 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)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +FileSelectorPlugin::FileSelectorPlugin( + FlutterRootWindowProvider window_provider, + std::unique_ptr dialog_controller_factory) + : get_root_window_(std::move(window_provider)), + controller_factory_(std::move(dialog_controller_factory)) {} + +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(); + } +} + +} // 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 new file mode 100644 index 000000000000..292d312bea30 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.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. +#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_SELECTOR_PLUGIN_H_ +#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_SELECTOR_PLUGIN_H_ + +#include +#include + +#include + +#include "file_dialog_controller.h" + +namespace file_selector_windows { + +// Abstraction for accessing the Flutter view's root window, to allow for faking +// in unit tests without creating fake window hierarchies, as well as to work +// around https://github.com/flutter/flutter/issues/90694. +using FlutterRootWindowProvider = std::function; + +class FileSelectorPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + // Creates a new plugin instance for the given registar, using the given + // factory to create native dialog controllers. + FileSelectorPlugin( + FlutterRootWindowProvider window_provider, + std::unique_ptr dialog_controller_factory); + + virtual ~FileSelectorPlugin(); + + // Called when a method is called on plugin channel; + void HandleMethodCall(const flutter::MethodCall<>& method_call, + std::unique_ptr> result); + + private: + // The provider for the root window to attach the dialog to. + FlutterRootWindowProvider get_root_window_; + + // The factory for creating dialog controller instances. + std::unique_ptr controller_factory_; +}; + +} // namespace file_selector_windows + +#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_FILE_SELECTOR_PLUGIN_H_ diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_windows.cpp b/packages/file_selector/file_selector_windows/windows/file_selector_windows.cpp new file mode 100644 index 000000000000..e4d2c15fd89b --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/file_selector_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/file_selector_windows/file_selector_windows.h" + +#include + +#include "file_selector_plugin.h" + +void FileSelectorWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + file_selector_windows::FileSelectorPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.h b/packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.h new file mode 100644 index 000000000000..7ee6ed3d29ff --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/include/file_selector_windows/file_selector_windows.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 PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_INCLUDE_FILE_SELECTOR_WINDOWS_FILE_SELECTOR_WINDOWS_H_ +#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_INCLUDE_FILE_SELECTOR_WINDOWS_FILE_SELECTOR_WINDOWS_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 FileSelectorWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_INCLUDE_FILE_SELECTOR_WINDOWS_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 new file mode 100644 index 000000000000..933500f34445 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/string_utils.cpp @@ -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. + +#include "string_utils.h" + +#include +#include + +#include + +namespace file_selector_windows { + +// Converts the given UTF-16 string to UTF-8. +std::string Utf8FromUtf16(const std::wstring& utf16_string) { + if (utf16_string.empty()) { + return std::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; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string.data(), + static_cast(utf16_string.length()), utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} + +// 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 file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/string_utils.h b/packages/file_selector/file_selector_windows/windows/string_utils.h new file mode 100644 index 000000000000..74c7d4f93934 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/string_utils.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. +#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_STRING_UTILS_H_ +#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_STRING_UTILS_H_ + +#include + +#include + +namespace file_selector_windows { + +// Converts the given UTF-16 string to UTF-8. +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); + +} // namespace file_selector_windows + +#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_STRING_UTILS_H_ 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 new file mode 100644 index 000000000000..20e1bba2794f --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp @@ -0,0 +1,471 @@ +// Copyright 2013 The Flutter 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 "file_selector_plugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "file_dialog_controller.h" +#include "string_utils.h" +#include "test/test_file_dialog_controller.h" +#include "test/test_utils.h" + +namespace file_selector_windows { +namespace test { + +namespace { + +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)); +}; + +} // namespace + +TEST(FileSelectorPlugin, TestOpenSimple) { + const HWND fake_window = reinterpret_cast(1337); + ScopedTestShellItem fake_selected_file; + IShellItemArrayPtr fake_result_array; + ::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, + HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate options. + FILEOPENDIALOGOPTIONS options; + dialog.GetOptions(&options); + EXPECT_EQ(options & FOS_ALLOWMULTISELECT, 0U); + EXPECT_EQ(options & FOS_PICKFOLDERS, 0U); + + 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)); + + EXPECT_TRUE(shown); +} + +TEST(FileSelectorPlugin, TestOpenWithArguments) { + const HWND fake_window = reinterpret_cast(1337); + ScopedTestShellItem fake_selected_file; + IShellItemArrayPtr fake_result_array; + ::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, + HWND parent) { + shown = true; + 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.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)); + + EXPECT_TRUE(shown); +} + +TEST(FileSelectorPlugin, TestOpenMultiple) { + const HWND fake_window = reinterpret_cast(1337); + ScopedTestFileIdList fake_selected_file_1; + ScopedTestFileIdList fake_selected_file_2; + LPCITEMIDLIST fake_selected_files[] = { + fake_selected_file_1.file(), + fake_selected_file_2.file(), + }; + IShellItemArrayPtr fake_result_array; + ::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, + HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate options. + FILEOPENDIALOGOPTIONS options; + dialog.GetOptions(&options); + EXPECT_NE(options & FOS_ALLOWMULTISELECT, 0U); + EXPECT_EQ(options & FOS_PICKFOLDERS, 0U); + + 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)); + + EXPECT_TRUE(shown); +} + +TEST(FileSelectorPlugin, TestOpenWithFilter) { + const HWND fake_window = reinterpret_cast(1337); + ScopedTestShellItem fake_selected_file; + IShellItemArrayPtr fake_result_array; + ::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")}, + })); + + bool shown = false; + MockShow show_validator = [&shown, fake_result_array, fake_window]( + const TestFileDialogController& dialog, + HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate filter. + const std::vector& filters = dialog.GetFileTypes(); + EXPECT_EQ(filters.size(), 3U); + if (filters.size() == 3U) { + EXPECT_EQ(filters[0].name, L"Text"); + EXPECT_EQ(filters[0].spec, L"*.txt;*.json"); + EXPECT_EQ(filters[1].name, L"Images"); + EXPECT_EQ(filters[1].spec, L"*.png;*.gif;*.jpeg"); + EXPECT_EQ(filters[2].name, L"Any"); + EXPECT_EQ(filters[2].spec, L"*.*"); + } + + 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)); + + EXPECT_TRUE(shown); +} + +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, + HWND parent) { + 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)); + + EXPECT_TRUE(shown); +} + +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]( + const TestFileDialogController& dialog, HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate options. + FILEOPENDIALOGOPTIONS options; + dialog.GetOptions(&options); + EXPECT_EQ(options & FOS_ALLOWMULTISELECT, 0U); + EXPECT_EQ(options & FOS_PICKFOLDERS, 0U); + + 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)); + + EXPECT_TRUE(shown); +} + +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]( + const TestFileDialogController& dialog, HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate arguments. + EXPECT_EQ(dialog.GetDefaultFolderPath(), L"C:\\Program Files"); + 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)); + + EXPECT_TRUE(shown); +} + +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, + HWND parent) { + 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)); + + EXPECT_TRUE(shown); +} + +TEST(FileSelectorPlugin, TestGetDirectorySimple) { + const HWND fake_window = reinterpret_cast(1337); + IShellItemPtr fake_selected_directory; + // This must be a directory that actually exists. + ::SHCreateItemFromParsingName(L"C:\\Program Files", nullptr, + IID_PPV_ARGS(&fake_selected_directory)); + IShellItemArrayPtr fake_result_array; + ::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, + HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate options. + FILEOPENDIALOGOPTIONS options; + dialog.GetOptions(&options); + EXPECT_EQ(options & FOS_ALLOWMULTISELECT, 0U); + EXPECT_NE(options & FOS_PICKFOLDERS, 0U); + + 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)); + + EXPECT_TRUE(shown); +} + +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, + HWND parent) { + 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)); + + EXPECT_TRUE(shown); +} + +} // namespace test +} // namespace file_selector_windows 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 new file mode 100644 index 000000000000..a98b686ddd6e --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp @@ -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. +#include "test/test_file_dialog_controller.h" + +#include + +#include +#include +#include + +namespace file_selector_windows { +namespace test { + +TestFileDialogController::TestFileDialogController(IFileDialog* dialog, + MockShow mock_show) + : dialog_(dialog), + mock_show_(std::move(mock_show)), + FileDialogController(dialog) {} + +TestFileDialogController::~TestFileDialogController() {} + +HRESULT TestFileDialogController::SetFileTypes(UINT count, + COMDLG_FILTERSPEC* filters) { + filter_groups_.clear(); + for (unsigned int i = 0; i < count; ++i) { + filter_groups_.push_back( + DialogFilter(filters[i].pszName, filters[i].pszSpec)); + } + return FileDialogController::SetFileTypes(count, filters); +} + +HRESULT TestFileDialogController::SetOkButtonLabel(const wchar_t* text) { + ok_button_label_ = text; + return FileDialogController::SetOkButtonLabel(text); +} + +HRESULT TestFileDialogController::Show(HWND parent) { + mock_result_ = mock_show_(*this, parent); + if (std::holds_alternative(mock_result_)) { + return HRESULT_FROM_WIN32(ERROR_CANCELLED); + } + return S_OK; +} + +HRESULT TestFileDialogController::GetResult(IShellItem** out_item) const { + *out_item = std::get(mock_result_); + (*out_item)->AddRef(); + return S_OK; +} + +HRESULT TestFileDialogController::GetResults( + IShellItemArray** out_items) const { + *out_items = std::get(mock_result_); + (*out_items)->AddRef(); + return S_OK; +} + +std::wstring TestFileDialogController::GetDefaultFolderPath() const { + IShellItemPtr item; + if (!SUCCEEDED(dialog_->GetFolder(&item))) { + return L""; + } + + wchar_t* path_chars = nullptr; + if (!SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path_chars))) { + return L""; + } + std::wstring path(path_chars); + ::CoTaskMemFree(path_chars); + return path; +} + +std::wstring TestFileDialogController::GetFileName() const { + wchar_t* name_chars = nullptr; + if (!SUCCEEDED(dialog_->GetFileName(&name_chars))) { + return L""; + } + std::wstring name(name_chars); + ::CoTaskMemFree(name_chars); + return name; +} + +const std::vector& TestFileDialogController::GetFileTypes() + const { + return filter_groups_; +} + +std::wstring TestFileDialogController::GetOkButtonLabel() const { + return ok_button_label_; +} + +// ---------------------------------------- + +TestFileDialogControllerFactory::TestFileDialogControllerFactory( + MockShow mock_show) + : mock_show_(std::move(mock_show)) {} +TestFileDialogControllerFactory::~TestFileDialogControllerFactory() {} + +std::unique_ptr +TestFileDialogControllerFactory::CreateController(IFileDialog* dialog) const { + return std::make_unique(dialog, mock_show_); +} + +} // namespace test +} // namespace file_selector_windows 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 new file mode 100644 index 000000000000..2e7292bad4b2 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h @@ -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. +#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_FILE_DIALOG_CONTROLLER_H_ +#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_FILE_DIALOG_CONTROLLER_H_ + +#include +#include +#include + +#include +#include +#include +#include + +#include "file_dialog_controller.h" +#include "test/test_utils.h" + +_COM_SMARTPTR_TYPEDEF(IFileDialog, IID_IFileDialog); + +namespace file_selector_windows { +namespace test { + +class TestFileDialogController; + +// A value to use for GetResult(s) in TestFileDialogController. The type depends +// on whether the dialog is an open or save dialog. +using MockShowResult = + std::variant; +// Called for TestFileDialogController::Show, to do validation and provide a +// mock return value for GetResult(s). +using MockShow = + std::function; + +// A C++-friendly version of a COMDLG_FILTERSPEC. +struct DialogFilter { + std::wstring name; + std::wstring spec; + + DialogFilter(const wchar_t* name, const wchar_t* spec) + : name(name), spec(spec) {} +}; + +// An extension of the normal file dialog controller that: +// - Allows for inspection of set values. +// - Allows faking the 'Show' interaction, providing tests an opportunity to +// validate the dialog settings and provide a return value, via MockShow. +class TestFileDialogController : public FileDialogController { + public: + TestFileDialogController(IFileDialog* dialog, MockShow mock_show); + ~TestFileDialogController(); + + // FileDialogController: + HRESULT SetFileTypes(UINT count, COMDLG_FILTERSPEC* filters) override; + HRESULT SetOkButtonLabel(const wchar_t* text) override; + HRESULT Show(HWND parent) override; + HRESULT GetResult(IShellItem** out_item) const override; + HRESULT GetResults(IShellItemArray** out_items) const override; + + // Accessors for validating IFileDialogController setter calls. + std::wstring GetDefaultFolderPath() const; + std::wstring GetFileName() const; + const std::vector& GetFileTypes() const; + std::wstring GetOkButtonLabel() const; + + private: + IFileDialogPtr dialog_; + MockShow mock_show_; + MockShowResult mock_result_; + + // The last set values, for IFileDialog properties that have setters but no + // corresponding getters. + std::wstring ok_button_label_; + std::vector filter_groups_; +}; + +// A controller factory that vends TestFileDialogController instances. +class TestFileDialogControllerFactory : public FileDialogControllerFactory { + public: + // Creates a factory whose instances use mock_show for the Show callback. + TestFileDialogControllerFactory(MockShow mock_show); + virtual ~TestFileDialogControllerFactory(); + + // Disallow copy and assign. + TestFileDialogControllerFactory(const TestFileDialogControllerFactory&) = + delete; + TestFileDialogControllerFactory& operator=( + const TestFileDialogControllerFactory&) = delete; + + // FileDialogControllerFactory: + std::unique_ptr CreateController( + IFileDialog* dialog) const override; + + private: + MockShow mock_show_; +}; + +} // namespace test +} // namespace file_selector_windows + +#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_FILE_DIALOG_CONTROLLER_H_ diff --git a/packages/file_selector/file_selector_windows/windows/test/test_main.cpp b/packages/file_selector/file_selector_windows/windows/test/test_main.cpp new file mode 100644 index 000000000000..5a49b52c1c76 --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/test/test_main.cpp @@ -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 +#include + +int main(int argc, char** argv) { + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + testing::InitGoogleTest(&argc, argv); + int exit_code = RUN_ALL_TESTS(); + + ::CoUninitialize(); + + return exit_code; +} diff --git a/packages/file_selector/file_selector_windows/windows/test/test_utils.cpp b/packages/file_selector/file_selector_windows/windows/test/test_utils.cpp new file mode 100644 index 000000000000..3e3ab98a734a --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/test/test_utils.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 "test/test_utils.h" + +#include +#include + +#include + +namespace file_selector_windows { +namespace test { + +namespace { + +// Creates a temp file and returns its path. +std::wstring CreateTempFile() { + wchar_t temp_dir[MAX_PATH]; + wchar_t temp_file[MAX_PATH]; + wchar_t long_path[MAX_PATH]; + ::GetTempPath(MAX_PATH, temp_dir); + ::GetTempFileName(temp_dir, L"test", 0, temp_file); + // Convert to long form to match what IShellItem queries will return. + ::GetLongPathName(temp_file, long_path, MAX_PATH); + return long_path; +} + +} // namespace + +ScopedTestShellItem::ScopedTestShellItem() { + path_ = CreateTempFile(); + ::SHCreateItemFromParsingName(path_.c_str(), nullptr, IID_PPV_ARGS(&item_)); +} + +ScopedTestShellItem::~ScopedTestShellItem() { ::DeleteFile(path_.c_str()); } + +ScopedTestFileIdList::ScopedTestFileIdList() { + path_ = CreateTempFile(); + item_ = ItemIdListPtr(::ILCreateFromPath(path_.c_str())); +} + +ScopedTestFileIdList::~ScopedTestFileIdList() { ::DeleteFile(path_.c_str()); } + +} // namespace test +} // namespace file_selector_windows diff --git a/packages/file_selector/file_selector_windows/windows/test/test_utils.h b/packages/file_selector/file_selector_windows/windows/test/test_utils.h new file mode 100644 index 000000000000..34106c50092f --- /dev/null +++ b/packages/file_selector/file_selector_windows/windows/test/test_utils.h @@ -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. +#ifndef PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_UTILS_H_ +#define PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_UTILS_H_ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "file_dialog_controller.h" + +_COM_SMARTPTR_TYPEDEF(IShellItem, IID_IShellItem); +_COM_SMARTPTR_TYPEDEF(IShellItemArray, IID_IShellItemArray); + +namespace file_selector_windows { +namespace test { + +// Creates a temp file, managed as an IShellItem, which will be deleted when +// the instance goes out of scope. +// +// This creates a file on the filesystem since creating IShellItem instances for +// files that don't exist is non-trivial. +class ScopedTestShellItem { + public: + ScopedTestShellItem(); + ~ScopedTestShellItem(); + + // Disallow copy and assign. + ScopedTestShellItem(const ScopedTestShellItem&) = delete; + ScopedTestShellItem& operator=(const ScopedTestShellItem&) = delete; + + // Returns the file's IShellItem reference. + IShellItemPtr file() { return item_; } + + // Returns the file's path. + const std::wstring& path() { return path_; } + + private: + IShellItemPtr item_; + std::wstring path_; +}; + +// Creates a temp file, managed as an ITEMIDLIST, which will be deleted when +// the instance goes out of scope. +// +// This creates a file on the filesystem since creating IShellItem instances for +// files that don't exist is non-trivial, and this is intended for use in +// creating IShellItemArray instances. +class ScopedTestFileIdList { + public: + ScopedTestFileIdList(); + ~ScopedTestFileIdList(); + + // Disallow copy and assign. + ScopedTestFileIdList(const ScopedTestFileIdList&) = delete; + ScopedTestFileIdList& operator=(const ScopedTestFileIdList&) = delete; + + // Returns the file's ITEMIDLIST reference. + PIDLIST_ABSOLUTE file() { return item_.get(); } + + // Returns the file's path. + const std::wstring& path() { return path_; } + + private: + // Smart pointer for managing ITEMIDLIST instances. + struct ItemIdListDeleter { + void operator()(LPITEMIDLIST item) { + if (item) { + ::ILFree(item); + } + } + }; + using ItemIdListPtr = std::unique_ptr, + ItemIdListDeleter>; + + ItemIdListPtr item_; + std::wstring path_; +}; + +} // namespace test +} // namespace file_selector_windows + +#endif // PACKAGES_FILE_SELECTOR_FILE_SELECTOR_WINDOWS_WINDOWS_TEST_TEST_UTILS_H_ diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 7e567d8cce5c..b86228532d00 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,6 +1,21 @@ -## NEXT +## 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. + +## 2.0.4 * Updated Android lint settings. +* Remove placeholder Dart file. ## 2.0.3 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/analysis_options.yaml b/packages/flutter_plugin_android_lifecycle/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/flutter_plugin_android_lifecycle/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 5a584b4e366f..9702604009f3 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' } } @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { minSdkVersion 16 @@ -53,7 +53,7 @@ android { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:1.10.19' } 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 da10d611c704..e96ede6844ff 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/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 29 + compileSdkVersion 31 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.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } 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 51f9a2537ffc..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 @@ -2,14 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter_plugin_android_lifecycle_example/main.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:flutter_plugin_android_lifecycle_example/main.dart'; 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 c019590b2a7c..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( @@ -16,7 +18,7 @@ class MyApp extends StatelessWidget { appBar: AppBar( title: const Text('Sample flutter_plugin_android_lifecycle usage'), ), - body: Center( + body: const Center( child: Text( 'This plugin only provides Android Lifecycle API\n for other Android plugins.')), ), diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index 6dc8d366e82b..0c88cd2c5531 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -17,10 +17,10 @@ dependencies: path: ../ dev_dependencies: - integration_test: - sdk: flutter flutter_test: sdk: flutter + integration_test: + sdk: flutter pedantic: ^1.8.0 flutter: diff --git a/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart b/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart deleted file mode 100644 index 340b06832f19..000000000000 --- a/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// The flutter_plugin_android_lifecycle plugin only provides a Java API -// for use by Android plugins. This plugin has no Dart code. diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index 0fc128d03e17..df8930dd9442 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. -repository: https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle +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.3 +version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: diff --git a/packages/google_maps_flutter/analysis_options.yaml b/packages/google_maps_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/google_maps_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/google_maps_flutter/google_maps_flutter/AUTHORS b/packages/google_maps_flutter/google_maps_flutter/AUTHORS index 493a0b4ef9c2..9f1b53ee2667 100644 --- a/packages/google_maps_flutter/google_maps_flutter/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Taha Tesser diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 5406dc50e04a..84f7867905ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,57 @@ +## NEXT + +* 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` +* Updates Android compileSdkVersion to 31. +* Internal code cleanup for stricter analysis options. + +## 2.1.1 + +* Suppresses unchecked cast warning. + +## 2.1.0 + +* Add iOS unit and UI integration test targets. +* Provide access to Hybrid Composition on Android through the `GoogleMap` widget. + +## 2.0.11 + +* Add additional marker drag events. + ## 2.0.10 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. @@ -191,7 +245,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 99c04f3ae1df..ae9a659c715f 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,9 +50,21 @@ 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. + +```dart +if (defaultTargetPlatform == TargetPlatform.android) { + AndroidGoogleMapsFlutter.useAndroidViewSurface = true; +} +``` + ### 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/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index e3cf6ffe8818..24502e5193c8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.4' } } @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { minSdkVersion 20 @@ -35,11 +35,11 @@ android { 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.0.2' 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 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:3.2.4' 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/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 056e10631011..2c2287cf59d4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -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; @@ -109,6 +111,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 +133,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 +303,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 +333,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 +345,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 +357,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 +437,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 +460,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 +472,7 @@ public void onSnapshotReady(Bitmap bitmap) { } case "tileOverlays#clearTileCache": { + invalidateMapIfNeeded(); String tileOverlayId = call.argument("tileOverlayId"); tileOverlaysController.clearTileCache(tileOverlayId); result.success(null); @@ -467,10 +537,14 @@ public boolean onMarkerClick(Marker marker) { } @Override - public void onMarkerDragStart(Marker marker) {} + public void onMarkerDragStart(Marker marker) { + markersController.onMarkerDragStart(marker.getId(), marker.getPosition()); + } @Override - public void onMarkerDrag(Marker marker) {} + public void onMarkerDrag(Marker marker) { + markersController.onMarkerDrag(marker.getId(), marker.getPosition()); + } @Override public void onMarkerDragEnd(Marker marker) { 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/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 189cba03c1cd..47ffe9b857d6 100644 --- 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/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -105,6 +105,28 @@ boolean onMarkerTap(String googleMarkerId) { return false; } + void onMarkerDragStart(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDragStart", data); + } + + void onMarkerDrag(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDrag", data); + } + void onMarkerDragEnd(String googleMarkerId, LatLng latLng) { String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); if (markerId == null) { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java index f05d04550994..73530d1b5158 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java @@ -74,6 +74,7 @@ Tile getTile() { } @Override + @SuppressWarnings("unchecked") public void success(Object data) { result = (Map) data; countDownLatch.countDown(); 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/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/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/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 index 6bda085caf46..d8082b57e3db 100644 --- 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 @@ -6,16 +6,24 @@ 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; @@ -58,4 +66,83 @@ public void OnDestroyReleaseTheMap() throws InterruptedException { 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/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java new file mode 100644 index 000000000000..3ca78e7674d7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java @@ -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. + +package io.flutter.plugins.googlemaps; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodCodec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.mockito.Mockito; + +public class MarkersControllerTest { + + @Test + public void controller_OnMarkerDragStart() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final Marker marker = mock(Marker.class); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDragStart(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDragStart", data); + } + + @Test + public void controller_OnMarkerDragEnd() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final Marker marker = mock(Marker.class); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDragEnd(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDragEnd", data); + } + + @Test + public void controller_OnMarkerDrag() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final Marker marker = mock(Marker.class); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDrag(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDrag", data); + } +} 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/src/test/java/io/flutter/plugins/googlemaps/PolygonControllerTest.java index 29234b6adb42..5c73a3f3e449 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/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.zzaa; 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 zzaa z = mock(zzaa.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/PolylineControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineControllerTest.java index bb7653aa2293..db570174e215 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/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.zzad; 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 zzad z = mock(zzad.class); final Polyline polyline = spy(new Polyline(z)); final float density = 5; 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.iml b/packages/google_maps_flutter/google_maps_flutter/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - 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 d850810db651..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 @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -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/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle index 456d020f6e2c..4d8d45d13a0b 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:3.5.4' } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml b/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml deleted file mode 100644 index 8070e6469054..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml b/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml deleted file mode 100644 index 0ca70ed93eaf..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/google_maps_flutter_example_android.iml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart index a4833fe8561d..fe3461b9142f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart @@ -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:typed_data'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -78,9 +80,9 @@ class GoogleMapInspector { } Future?> getTileOverlayInfo(String id) async { - return (await _channel.invokeMapMethod( + 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 8bafca15c344..8742ff31af42 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 @@ -7,11 +7,10 @@ import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:integration_test/integration_test.dart'; 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:integration_test/integration_test.dart'; import 'google_map_inspector.dart'; @@ -53,7 +52,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, compassEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -93,7 +92,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, mapToolbarEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -140,7 +139,8 @@ void main() { final GoogleMapInspector inspector = await inspectorCompleter.future; if (Platform.isIOS) { - MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); + final MinMaxZoomPreference zoomLevel = + await inspector.getMinMaxZoomLevels(); expect(zoomLevel, equals(initialZoomLevel)); } else if (Platform.isAndroid) { await controller.moveCamera(CameraUpdate.zoomTo(15)); @@ -161,13 +161,14 @@ void main() { initialCameraPosition: _kInitialCameraPosition, minMaxZoomPreference: finalZoomLevel, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); if (Platform.isIOS) { - MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); + final MinMaxZoomPreference zoomLevel = + await inspector.getMinMaxZoomLevels(); expect(zoomLevel, equals(finalZoomLevel)); } else { await controller.moveCamera(CameraUpdate.zoomTo(15)); @@ -213,7 +214,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, zoomGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -243,7 +244,7 @@ void main() { final GoogleMapInspector inspector = await inspectorCompleter.future; bool? zoomControlsEnabled = await inspector.isZoomControlsEnabled(); - expect(zoomControlsEnabled, Platform.isIOS ? false : true); + expect(zoomControlsEnabled, !Platform.isIOS); /// Zoom Controls functionality is not available on iOS at the moment. if (Platform.isAndroid) { @@ -254,7 +255,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, zoomControlsEnabled: false, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -295,7 +296,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, liteModeEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -335,7 +336,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, rotateGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -375,7 +376,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, tiltGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -415,7 +416,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, scrollGesturesEnabled: true, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -425,7 +426,8 @@ void main() { }); 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(); @@ -449,11 +451,11 @@ void main() { // 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(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); - ScreenCoordinate coordinate = + final ScreenCoordinate coordinate = await mapController.getScreenCoordinate(_kInitialCameraPosition.target); - Rect rect = tester.getRect(find.byKey(key)); + final Rect rect = tester.getRect(find.byKey(key)); if (Platform.isIOS) { // On iOS, the coordinate value from the GoogleMapSdk doesn't include the devicePixelRatio`. // So we don't need to do the conversion like we did below for other platforms. @@ -472,6 +474,7 @@ void main() { .round()); } await tester.binding.setSurfaceSize(null); + AndroidGoogleMapsFlutter.useAndroidViewSurface = false; }); testWidgets('testGetVisibleRegion', (WidgetTester tester) async { @@ -525,7 +528,7 @@ void main() { // TODO(iskakaushik): non-zero padding is needed for some device configurations // https://github.com/flutter/flutter/issues/30575 - final double padding = 0; + const double padding = 0; await mapController .moveCamera(CameraUpdate.newLatLngBounds(latLngBounds, padding)); await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -573,7 +576,7 @@ void main() { initialCameraPosition: _kInitialCameraPosition, trafficEnabled: false, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -641,7 +644,7 @@ void main() { myLocationButtonEnabled: false, myLocationEnabled: false, onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); + fail('OnMapCreated should get called only once.'); }, ), )); @@ -723,7 +726,7 @@ void main() { )); final GoogleMapController controller = await controllerCompleter.future; - final String mapStyle = + const String mapStyle = '[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]}]'; await controller.setMapStyle(mapStyle); }); @@ -797,7 +800,7 @@ void main() { // 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(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); final LatLngBounds visibleRegion = await controller.getVisibleRegion(); final LatLng topLeft = @@ -832,7 +835,7 @@ void main() { // 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(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); double zoom = await controller.getZoomLevel(); expect(zoom, _kInitialZoomLevel); @@ -864,7 +867,7 @@ void main() { // 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(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); final LatLngBounds visibleRegion = await controller.getVisibleRegion(); final LatLng northWest = LatLng( @@ -902,7 +905,7 @@ void main() { // 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(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); // Simple call to make sure that the app hasn't crashed. final LatLngBounds bounds1 = await controller.getVisibleRegion(); @@ -911,12 +914,12 @@ void main() { }); testWidgets('testToggleInfoWindow', (WidgetTester tester) async { - final Marker marker = Marker( - markerId: MarkerId("marker"), - infoWindow: InfoWindow(title: "InfoWindow")); + const Marker marker = Marker( + markerId: MarkerId('marker'), + infoWindow: InfoWindow(title: 'InfoWindow')); final Set markers = {marker}; - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); await tester.pumpWidget(Directionality( @@ -930,7 +933,7 @@ void main() { ), )); - GoogleMapController controller = await controllerCompleter.future; + final GoogleMapController controller = await controllerCompleter.future; bool iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); @@ -945,9 +948,9 @@ void main() { expect(iwVisibleStatus, false); }); - testWidgets("fromAssetImage", (WidgetTester tester) async { - double pixelRatio = 2; - final ImageConfiguration imageConfiguration = + 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'); @@ -959,7 +962,7 @@ void main() { }); testWidgets('testTakeSnapshot', (WidgetTester tester) async { - Completer inspectorCompleter = + final Completer inspectorCompleter = Completer(); await tester.pumpWidget( @@ -990,10 +993,10 @@ void main() { testWidgets( 'set tileOverlay correctly', (WidgetTester tester) async { - Completer inspectorCompleter = + final Completer inspectorCompleter = Completer(); final TileOverlay tileOverlay1 = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), + tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 2, visible: true, @@ -1002,7 +1005,7 @@ void main() { ); final TileOverlay tileOverlay2 = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_2'), + tileOverlayId: const TileOverlayId('tile_overlay_2'), tileProvider: _DebugTileProvider(), zIndex: 1, visible: false, @@ -1028,9 +1031,9 @@ void main() { final GoogleMapInspector inspector = await inspectorCompleter.future; - Map tileOverlayInfo1 = + final Map tileOverlayInfo1 = (await inspector.getTileOverlayInfo('tile_overlay_1'))!; - Map tileOverlayInfo2 = + final Map tileOverlayInfo2 = (await inspector.getTileOverlayInfo('tile_overlay_2'))!; expect(tileOverlayInfo1['visible'], isTrue); @@ -1050,11 +1053,11 @@ void main() { testWidgets( 'update tileOverlays correctly', (WidgetTester tester) async { - Completer inspectorCompleter = + final Completer inspectorCompleter = Completer(); final Key key = GlobalKey(); final TileOverlay tileOverlay1 = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), + tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 2, visible: true, @@ -1063,7 +1066,7 @@ void main() { ); final TileOverlay tileOverlay2 = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_2'), + tileOverlayId: const TileOverlayId('tile_overlay_2'), tileProvider: _DebugTileProvider(), zIndex: 3, visible: true, @@ -1090,7 +1093,7 @@ void main() { final GoogleMapInspector inspector = await inspectorCompleter.future; final TileOverlay tileOverlay1New = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), + tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 1, visible: false, @@ -1114,9 +1117,9 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 3)); - Map tileOverlayInfo1 = + final Map tileOverlayInfo1 = (await inspector.getTileOverlayInfo('tile_overlay_1'))!; - Map? tileOverlayInfo2 = + final Map? tileOverlayInfo2 = await inspector.getTileOverlayInfo('tile_overlay_2'); expect(tileOverlayInfo1['visible'], isFalse); @@ -1132,11 +1135,11 @@ void main() { testWidgets( 'remove tileOverlays correctly', (WidgetTester tester) async { - Completer inspectorCompleter = + final Completer inspectorCompleter = Completer(); final Key key = GlobalKey(); final TileOverlay tileOverlay1 = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), + tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), zIndex: 2, visible: true, @@ -1177,7 +1180,7 @@ void main() { ); await tester.pumpAndSettle(const Duration(seconds: 3)); - Map? tileOverlayInfo1 = + final Map? tileOverlayInfo1 = await inspector.getTileOverlayInfo('tile_overlay_1'); expect(tileOverlayInfo1, isNull); @@ -1196,7 +1199,7 @@ class _DebugTileProvider implements TileProvider { static const int width = 100; static const int height = 100; static final Paint boxPaint = Paint(); - static final TextStyle textStyle = TextStyle( + static const TextStyle textStyle = TextStyle( color: Colors.red, fontSize: 20, ); @@ -1206,7 +1209,7 @@ class _DebugTileProvider implements TileProvider { final ui.PictureRecorder recorder = ui.PictureRecorder(); final Canvas canvas = Canvas(recorder); final TextSpan textSpan = TextSpan( - text: "$x,$y", + text: '$x,$y', style: textStyle, ); final TextPainter textPainter = TextPainter( @@ -1217,7 +1220,7 @@ class _DebugTileProvider implements TileProvider { minWidth: 0.0, maxWidth: width.toDouble(), ); - final Offset offset = const Offset(0, 0); + const Offset offset = Offset(0, 0); textPainter.paint(canvas, offset); canvas.drawRect( Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); 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..29bfe631a3e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile @@ -31,6 +31,8 @@ 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/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..b37246b98a47 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,14 @@ 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 */; }; + 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 +57,8 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 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; }; 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,6 +72,8 @@ 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 = ""; }; 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 = ""; }; @@ -92,6 +99,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */, FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -109,6 +117,7 @@ 1E7CF0857EFC88FC263CF3B2 /* Frameworks */ = { isa = PBXGroup; children = ( + 68E472692836FF0C00BDDDAC /* MapKit.framework */, 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, ); @@ -187,7 +196,10 @@ F7151F11265D7ED70028CB91 /* RunnerTests */ = { isa = PBXGroup; children = ( + 6851F3552835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m */, F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */, F7151F14265D7ED70028CB91 /* Info.plist */, ); path = RunnerTests; @@ -270,7 +282,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1320; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -441,6 +453,8 @@ buildActionMask = 2147483647; files = ( F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */, + 6851F3562835BC180032B7C8 /* FLTGoogleMapJSONConversionsConversionTests.m in Sources */, + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -511,6 +525,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 +582,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 +659,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 +675,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; 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 @@ #import "AppDelegate.h" -int main(int argc, char* argv[]) { +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/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m new file mode 100644 index 000000000000..bf226feb2341 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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; +@import google_maps_flutter.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/example/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m index 5249145f0c87..f03dca24fe7c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.m @@ -3,16 +3,40 @@ // found in the LICENSE file. @import google_maps_flutter; +@import google_maps_flutter.Test; @import XCTest; +#import +#import "PartiallyMockedMapView.h" + @interface GoogleMapsTests : XCTestCase @end @implementation GoogleMapsTests - (void)testPlugin { - FLTGoogleMapsPlugin* plugin = [[FLTGoogleMapsPlugin alloc] init]; + 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); +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.h new file mode 100644 index 000000000000..4288401cf90d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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/example/ios/RunnerTests/PartiallyMockedMapView.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/PartiallyMockedMapView.m new file mode 100644 index 000000000000..202a18d128c0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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/example/ios/RunnerUITests/GoogleMapsUITests.m index f56a2d17e3fe..2f0c0fa6d615 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.m @@ -6,7 +6,7 @@ @import os.log; @interface GoogleMapsUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @implementation GoogleMapsUITests @@ -17,11 +17,13 @@ - (void)setUp { self.app = [[XCUIApplication alloc] init]; [self.app launch]; + // The location permission interception is currently not working. + // See: https://github.com/flutter/flutter/issues/93325. [self addUIInterruptionMonitorWithDescription:@"Permission popups" - handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { + handler:^BOOL(XCUIElement *_Nonnull interruptingElement) { if (@available(iOS 14, *)) { - XCUIElement* locationPermission = + XCUIElement *locationPermission = interruptingElement.buttons[@"Allow While Using App"]; if (![locationPermission waitForExistenceWithTimeout:30.0]) { @@ -31,7 +33,7 @@ - (void)setUp { [locationPermission tap]; } else { - XCUIElement* allow = + XCUIElement *allow = interruptingElement.buttons[@"Allow"]; if (![allow waitForExistenceWithTimeout:30.0]) { XCTFail(@"Failed due to not able to find Allow button"); @@ -42,20 +44,21 @@ - (void)setUp { }]; } -- (void)testUserInterface { - XCUIApplication* app = self.app; - XCUIElement* userInteface = app.staticTexts[@"User interface"]; +// Temporarily disabled due to https://github.com/flutter/flutter/issues/93325 +- (void)skip_testUserInterface { + XCUIApplication *app = self.app; + XCUIElement *userInteface = app.staticTexts[@"User interface"]; if (![userInteface waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find User interface"); } [userInteface tap]; - XCUIElement* platformView = app.otherElements[@"platform_view[0]"]; + XCUIElement *platformView = app.otherElements[@"platform_view[0]"]; if (![platformView waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find platform view"); } - XCUIElement* compass = app.buttons[@"disable compass"]; + XCUIElement *compass = app.buttons[@"disable compass"]; if (![compass waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find compass button"); @@ -63,4 +66,59 @@ - (void)testUserInterface { [compass tap]; } +- (void)testMapCoordinatesPage { + XCUIApplication *app = self.app; + XCUIElement *mapCoordinates = app.staticTexts[@"Map coordinates"]; + if (![mapCoordinates waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'Map coordinates''"); + } + [mapCoordinates tap]; + + XCUIElement *platformView = app.otherElements[@"platform_view[0]"]; + if (![platformView waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find platform view"); + } + + XCUIElement *getVisibleRegionBoundsButton = app.buttons[@"Get Visible Region Bounds"]; + if (![getVisibleRegionBoundsButton waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'Get Visible Region Bounds''"); + } + [getVisibleRegionBoundsButton tap]; +} + +- (void)testMapClickPage { + XCUIApplication *app = self.app; + XCUIElement *mapClick = app.staticTexts[@"Map click"]; + if (![mapClick waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'Map click''"); + } + [mapClick tap]; + + XCUIElement *platformView = app.otherElements[@"platform_view[0]"]; + if (![platformView waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find platform view"); + } + + [platformView tap]; + + XCUIElement *tapped = app.staticTexts[@"Tapped"]; + if (![tapped waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'tapped''"); + } + + [platformView pressForDuration:5.0]; + + XCUIElement *longPressed = app.staticTexts[@"Long pressed"]; + if (![longPressed waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find 'longPressed''"); + } +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart index cc5fd257dfd3..3975d64449b8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart @@ -10,8 +10,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class AnimateCameraPage extends GoogleMapExampleAppPage { - AnimateCameraPage() - : super(const Icon(Icons.map), 'Camera control, animated'); + const AnimateCameraPage({Key? key}) + : super(const Icon(Icons.map), 'Camera control, animated', key: key); @override Widget build(BuildContext context) { @@ -20,7 +20,7 @@ class AnimateCameraPage extends GoogleMapExampleAppPage { } class AnimateCamera extends StatefulWidget { - const AnimateCamera(); + const AnimateCamera({Key? key}) : super(key: key); @override State createState() => 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 f6d6f54e135a..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 { - 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) { @@ -26,9 +26,9 @@ class _LiteModeBody extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( + return const Card( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30.0), + padding: EdgeInsets.symmetric(vertical: 30.0), child: Center( child: SizedBox( width: 300.0, diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 15b14db0357a..8932705bc6d5 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,9 +2,10 @@ // 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/google_maps_flutter.dart'; import 'package:google_maps_flutter_example/lite_mode.dart'; import 'animate_camera.dart'; import 'map_click.dart'; @@ -23,24 +24,28 @@ import 'snapshot.dart'; import 'tile_overlay.dart'; final List _allPages = [ - MapUiPage(), - MapCoordinatesPage(), - MapClickPage(), - AnimateCameraPage(), - MoveCameraPage(), - PlaceMarkerPage(), - MarkerIconsPage(), - ScrollingMapPage(), - PlacePolylinePage(), - PlacePolygonPage(), - PlaceCirclePage(), - PaddingPage(), - SnapshotPage(), - LiteModePage(), - TileOverlayPage(), + 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( @@ -66,5 +71,8 @@ class MapsDemo extends StatelessWidget { } void main() { - runApp(MaterialApp(home: MapsDemo())); + if (defaultTargetPlatform == TargetPlatform.android) { + AndroidGoogleMapsFlutter.useAndroidViewSurface = true; + } + 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 a46fc5fba420..9c96f25d5fa7 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 { - 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) { @@ -68,15 +68,27 @@ class _MapClickBodyState extends State<_MapClickBody> { 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, + 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( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -84,7 +96,7 @@ class _MapClickBodyState extends State<_MapClickBody> { ); } - void onMapCreated(GoogleMapController controller) async { + Future onMapCreated(GoogleMapController controller) async { setState(() { mapController = controller; }); 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 99ab16802fea..1179acd46ffa 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 { - 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) { @@ -72,7 +72,7 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { ); } - void onMapCreated(GoogleMapController controller) async { + Future onMapCreated(GoogleMapController controller) async { final LatLngBounds visibleRegion = await controller.getVisibleRegion(); setState(() { mapController = controller; 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 2e0d2d188a3f..fbfeda56a968 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 { - 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(); @@ -34,14 +35,14 @@ class MapUiBody extends StatefulWidget { class MapUiBodyState extends State { MapUiBodyState(); - static final CameraPosition _kInitialPosition = const CameraPosition( + static const CameraPosition _kInitialPosition = CameraPosition( target: LatLng(-33.852, 151.211), zoom: 11.0, ); CameraPosition _position = _kInitialPosition; bool _isMapCreated = false; - bool _isMoving = false; + final bool _isMoving = false; bool _compassEnabled = true; bool _mapToolbarEnabled = true; CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded; 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 da57b83a7e4f..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 { - 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(); @@ -60,13 +61,13 @@ class MarkerIconsBodyState extends State { Marker _createMarker() { if (_markerIcon != null) { return Marker( - markerId: MarkerId("marker_1"), + markerId: const MarkerId('marker_1'), position: _kMapCenter, icon: _markerIcon!, ); } else { - return Marker( - markerId: MarkerId("marker_1"), + return const Marker( + markerId: MarkerId('marker_1'), position: _kMapCenter, ); } @@ -75,7 +76,7 @@ class MarkerIconsBodyState extends State { Future _createMarkerImageFromAsset(BuildContext context) async { if (_markerIcon == null) { final ImageConfiguration imageConfiguration = - createLocalImageConfiguration(context, size: Size.square(48)); + createLocalImageConfiguration(context, size: const Size.square(48)); BitmapDescriptor.fromAssetImage( imageConfiguration, 'assets/red_square.png') .then(_updateBitmap); 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 f8274196770d..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 { - 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 d90005fa6998..a4bfa88d559f 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 { - 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(); @@ -53,11 +54,11 @@ class MarkerIconsBodyState extends State { ), ), ), - Padding( - padding: const EdgeInsets.only(top: 20), + const Padding( + padding: EdgeInsets.only(top: 20), child: Center( child: Text( - "Enter Padding Below", + 'Enter Padding Below', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ), @@ -79,10 +80,10 @@ class MarkerIconsBodyState extends State { }); } - TextEditingController _topController = TextEditingController(); - TextEditingController _bottomController = TextEditingController(); - TextEditingController _leftController = TextEditingController(); - TextEditingController _rightController = TextEditingController(); + final TextEditingController _topController = TextEditingController(); + final TextEditingController _bottomController = TextEditingController(); + final TextEditingController _leftController = TextEditingController(); + final TextEditingController _rightController = TextEditingController(); Widget _paddingInput() { return Padding( @@ -96,7 +97,7 @@ class MarkerIconsBodyState extends State { keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: const InputDecoration( - hintText: "Top", + hintText: 'Top', ), ), ), @@ -108,7 +109,7 @@ class MarkerIconsBodyState extends State { keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: const InputDecoration( - hintText: "Bottom", + hintText: 'Bottom', ), ), ), @@ -120,7 +121,7 @@ class MarkerIconsBodyState extends State { keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: const InputDecoration( - hintText: "Left", + hintText: 'Left', ), ), ), @@ -132,7 +133,7 @@ class MarkerIconsBodyState extends State { keyboardType: TextInputType.number, textAlign: TextAlign.center, decoration: const InputDecoration( - hintText: "Right", + hintText: 'Right', ), ), ), @@ -148,7 +149,7 @@ class MarkerIconsBodyState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( - child: const Text("Set Padding"), + child: const Text('Set Padding'), onPressed: () { setState(() { _padding = EdgeInsets.fromLTRB( @@ -160,7 +161,7 @@ class MarkerIconsBodyState extends State { }, ), TextButton( - child: const Text("Reset Padding"), + child: const Text('Reset Padding'), onPressed: () { setState(() { _topController.clear(); 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 a4953428f088..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,7 +10,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlaceCirclePage extends GoogleMapExampleAppPage { - 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) { @@ -19,7 +20,7 @@ class PlaceCirclePage extends GoogleMapExampleAppPage { } class PlaceCircleBody extends StatefulWidget { - const PlaceCircleBody(); + const PlaceCircleBody({Key? key}) : super(key: key); @override State createState() => PlaceCircleBodyState(); @@ -47,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; } @@ -169,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 3d083e5f9fa9..b8efc4e52562 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 @@ -6,8 +6,8 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui'; import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -15,7 +15,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlaceMarkerPage extends GoogleMapExampleAppPage { - 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,23 +25,25 @@ class PlaceMarkerPage extends GoogleMapExampleAppPage { } class PlaceMarkerBody extends StatefulWidget { - const PlaceMarkerBody(); + const PlaceMarkerBody({Key? key}) : super(key: key); @override State createState() => PlaceMarkerBodyState(); } -typedef Marker MarkerUpdateAction(Marker marker); +typedef MarkerUpdateAction = Marker Function(Marker marker); class PlaceMarkerBodyState extends State { PlaceMarkerBodyState(); - static final LatLng center = const LatLng(-33.86711, 151.1947171); + static const LatLng center = LatLng(-33.86711, 151.1947171); GoogleMapController? controller; Map markers = {}; MarkerId? selectedMarker; int _markerIdCounter = 1; + LatLng? markerPosition; + // ignore: use_setters_to_change_properties void _onMapCreated(GoogleMapController controller) { this.controller = controller; } @@ -67,13 +70,24 @@ class PlaceMarkerBodyState extends State { ), ); markers[markerId] = newMarker; + + markerPosition = null; }); } } - void _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { + 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) { @@ -115,12 +129,9 @@ class PlaceMarkerBodyState extends State { center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, ), infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), - onTap: () { - _onMarkerTapped(markerId); - }, - onDragEnd: (LatLng position) { - _onMarkerDragEnd(markerId, position); - }, + onTap: () => _onMarkerTapped(markerId), + onDragEnd: (LatLng position) => _onMarkerDragEnd(markerId, position), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), ); setState(() { @@ -197,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( @@ -280,14 +291,12 @@ class PlaceMarkerBodyState extends State { @override Widget build(BuildContext context) { final MarkerId? selectedId = selectedMarker; - return Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center( - child: SizedBox( - width: 300.0, - height: 200.0, + return Stack(children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( child: GoogleMap( onMapCreated: _onMapCreated, initialCameraPosition: const CameraPosition( @@ -297,111 +306,116 @@ class PlaceMarkerBodyState extends State { markers: Set.of(markers.values), ), ), - ), - Expanded( - child: SingleChildScrollView( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - children: [ - Column( - children: [ - TextButton( - child: const Text('add'), - onPressed: _add, - ), - TextButton( - child: const Text('remove'), - onPressed: selectedId == null - ? null - : () => _remove(selectedId), - ), - TextButton( - child: const Text('change info'), - onPressed: selectedId == null - ? null - : () => _changeInfo(selectedId), - ), - TextButton( - child: const Text('change info anchor'), - onPressed: selectedId == null - ? null - : () => _changeInfoAnchor(selectedId), - ), - ], - ), - Column( - children: [ - TextButton( - child: const Text('change alpha'), - onPressed: selectedId == null - ? null - : () => _changeAlpha(selectedId), - ), - TextButton( - child: const Text('change anchor'), - onPressed: selectedId == null - ? null - : () => _changeAnchor(selectedId), - ), - TextButton( - child: const Text('toggle draggable'), - onPressed: selectedId == null - ? null - : () => _toggleDraggable(selectedId), - ), - TextButton( - child: const Text('toggle flat'), - onPressed: selectedId == null - ? null - : () => _toggleFlat(selectedId), - ), - TextButton( - child: const Text('change position'), - onPressed: selectedId == null - ? null - : () => _changePosition(selectedId), - ), - TextButton( - child: const Text('change rotation'), - onPressed: selectedId == null - ? null - : () => _changeRotation(selectedId), - ), - TextButton( - child: const Text('toggle visible'), - onPressed: selectedId == null - ? null - : () => _toggleVisible(selectedId), - ), - TextButton( - child: const Text('change zIndex'), - onPressed: selectedId == null - ? null - : () => _changeZIndex(selectedId), - ), - TextButton( - child: const Text('set marker icon'), - onPressed: selectedId == null - ? null - : () { - _getAssetIcon(context).then( - (BitmapDescriptor icon) { - _setMarkerIcon(selectedId, icon); - }, - ); - }, - ), - ], - ), - ], - ) - ], - ), + 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, + mainAxisSize: MainAxisSize.max, + 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/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart index 476084defa75..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,7 +10,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlacePolygonPage extends GoogleMapExampleAppPage { - 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) { @@ -19,7 +20,7 @@ class PlacePolygonPage extends GoogleMapExampleAppPage { } class PlacePolygonBody extends StatefulWidget { - const PlacePolygonBody(); + const PlacePolygonBody({Key? key}) : super(key: key); @override State createState() => PlacePolygonBodyState(); @@ -48,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; } @@ -195,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 aeb9bf1b11eb..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,7 +11,8 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; class PlacePolylinePage extends GoogleMapExampleAppPage { - 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) { @@ -20,7 +21,7 @@ class PlacePolylinePage extends GoogleMapExampleAppPage { } class PlacePolylineBody extends StatefulWidget { - const PlacePolylineBody(); + const PlacePolylineBody({Key? key}) : super(key: key); @override State createState() => PlacePolylineBodyState(); @@ -76,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; } @@ -233,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 9611d36bc8e8..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,13 +7,15 @@ 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'; +const LatLng _center = LatLng(32.080664, 34.9563837); + class ScrollingMapPage extends GoogleMapExampleAppPage { - 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) { @@ -22,9 +24,7 @@ class ScrollingMapPage extends GoogleMapExampleAppPage { } class ScrollingMapBody extends StatelessWidget { - const ScrollingMapBody(); - - final LatLng center = const LatLng(32.080664, 34.9563837); + const ScrollingMapBody({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -44,8 +44,8 @@ class ScrollingMapBody extends StatelessWidget { width: 300.0, height: 300.0, child: GoogleMap( - initialCameraPosition: CameraPosition( - target: center, + initialCameraPosition: const CameraPosition( + target: _center, zoom: 11.0, ), gestureRecognizers: // @@ -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: @@ -77,16 +77,16 @@ class ScrollingMapBody extends StatelessWidget { width: 300.0, height: 300.0, child: GoogleMap( - initialCameraPosition: CameraPosition( - target: center, + initialCameraPosition: const CameraPosition( + target: _center, zoom: 11.0, ), markers: { Marker( - markerId: MarkerId("test_marker_id"), + markerId: const MarkerId('test_marker_id'), position: LatLng( - center.latitude, - center.longitude, + _center.latitude, + _center.longitude, ), infoWindow: const InfoWindow( title: 'An interesting location', 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 c85048f5b5aa..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 { - 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) { @@ -39,7 +40,7 @@ class _SnapshotBodyState extends State<_SnapshotBody> { padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + children: [ SizedBox( height: 180, child: GoogleMap( @@ -48,9 +49,10 @@ class _SnapshotBodyState extends State<_SnapshotBody> { ), ), TextButton( - child: Text('Take a snapshot'), + child: const Text('Take a snapshot'), onPressed: () async { - final imageBytes = await _mapController?.takeSnapshot(); + final Uint8List? imageBytes = + await _mapController?.takeSnapshot(); setState(() { _imageBytes = imageBytes; }); @@ -66,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 1d6dd69c186b..d88e09988dc7 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 { - 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; } @@ -51,7 +53,7 @@ class TileOverlayBodyState extends State { void _addTileOverlay() { final TileOverlay tileOverlay = TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), + tileOverlayId: const TileOverlayId('tile_overlay_1'), tileProvider: _DebugTileProvider(), ); setState(() { @@ -67,7 +69,7 @@ class TileOverlayBodyState extends State { @override Widget build(BuildContext context) { - Set overlays = { + final Set overlays = { if (_tileOverlay != null) _tileOverlay!, }; return Column( @@ -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'), ), ], ); @@ -117,7 +119,7 @@ class _DebugTileProvider implements TileProvider { static const int width = 100; static const int height = 100; static final Paint boxPaint = Paint(); - static final TextStyle textStyle = TextStyle( + static const TextStyle textStyle = TextStyle( color: Colors.red, fontSize: 20, ); @@ -138,7 +140,7 @@ class _DebugTileProvider implements TileProvider { minWidth: 0.0, maxWidth: width.toDouble(), ); - final Offset offset = const Offset(0, 0); + const Offset offset = Offset(0, 0); textPainter.paint(canvas, offset); canvas.drawRect( Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); 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 cd614c7e4384..196f054e1fc0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -4,13 +4,13 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" dependencies: + cupertino_icons: ^0.1.0 flutter: sdk: flutter - - cupertino_icons: ^0.1.0 + flutter_plugin_android_lifecycle: ^2.0.1 google_maps_flutter: # When depending on this package from a real application you should use: # google_maps_flutter: ^x.y.z @@ -18,7 +18,6 @@ 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: ../ - flutter_plugin_android_lifecycle: ^2.0.1 dev_dependencies: espresso: ^0.1.0+2 @@ -26,7 +25,6 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml b/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml deleted file mode 100644 index 0fbaf2c3a822..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml b/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml deleted file mode 100644 index 0ebb6c9fe763..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/google_mobile_maps_android.iml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapJSONConversions.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapJSONConversions.h new file mode 100644 index 000000000000..cfccb7b0b5f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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/Classes/FLTGoogleMapJSONConversions.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapJSONConversions.m new file mode 100644 index 000000000000..d554c501b1e2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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/Classes/FLTGoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h index 356a13faba62..5dcc66594f18 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h @@ -7,25 +7,19 @@ 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; +@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 *tileOverlayId; -- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId; +@property(copy, nonatomic, readonly) NSString *tileOverlayIdentifier; +- (instancetype)init:(FlutterMethodChannel *)methodChannel + withTileOverlayIdentifier:(NSString *)identifier; @end @interface FLTTileOverlaysController : NSObject @@ -34,9 +28,9 @@ NS_ASSUME_NONNULL_BEGIN registrar:(NSObject *)registrar; - (void)addTileOverlays:(NSArray *)tileOverlaysToAdd; - (void)changeTileOverlays:(NSArray *)tileOverlaysToChange; -- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove; -- (void)clearTileCache:(NSString *)tileOverlayId; -- (nullable NSDictionary *)getTileOverlayInfo:(NSString *)tileverlayId; +- (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/Classes/FLTGoogleMapTileOverlayController.m index fb391380c92c..5863697d7b9b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m @@ -3,51 +3,25 @@ // 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 () -@property(strong, nonatomic) GMSTileLayer* layer; -@property(weak, nonatomic) GMSMapView* mapView; +@property(strong, nonatomic) GMSTileLayer *layer; +@property(weak, nonatomic) GMSMapView *mapView; @end @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; } @@ -60,8 +34,8 @@ - (void)clearTileCache { [self.layer clearTileCache]; } -- (NSDictionary*)getTileOverlayInfo { - NSMutableDictionary* info = [[NSMutableDictionary alloc] init]; +- (NSDictionary *)getTileOverlayInfo { + NSMutableDictionary *info = [[NSMutableDictionary alloc] init]; BOOL visible = self.layer.map != nil; info[@"visible"] = @(visible); info[@"fadeIn"] = @(self.layer.fadeIn); @@ -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,15 +125,15 @@ - (void)requestTileForX:(NSUInteger)x [self.methodChannel invokeMethod:@"tileOverlay#getTile" arguments:@{ - @"tileOverlayId" : self.tileOverlayId, + @"tileOverlayId" : self.tileOverlayIdentifier, @"x" : @(x), @"y" : @(y), @"zoom" : @(zoom) } result:^(id _Nullable result) { - UIImage* tileImage; + UIImage *tileImage; if ([result isKindOfClass:[NSDictionary class]]) { - FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)result[@"data"]; + FlutterStandardTypedData *typedData = (FlutterStandardTypedData *)result[@"data"]; if (typedData == nil) { tileImage = kGMSTileLayerNoTile; } else { @@ -138,7 +141,7 @@ - (void)requestTileForX:(NSUInteger)x } } else { if ([result isKindOfClass:[FlutterError class]]) { - FlutterError* error = (FlutterError*)result; + FlutterError *error = (FlutterError *)result; NSLog(@"Can't get tile: errorCode = %@, errorMessage = %@, details = %@", [error code], [error message], [error details]); } @@ -156,78 +159,80 @@ - (void)requestTileForX:(NSUInteger)x @interface FLTTileOverlaysController () -@property(strong, nonatomic) NSMutableDictionary* tileOverlayIdToController; -@property(weak, nonatomic) FlutterMethodChannel* methodChannel; -@property(weak, nonatomic) NSObject* registrar; -@property(weak, nonatomic) GMSMapView* mapView; +@property(strong, nonatomic) NSMutableDictionary *tileOverlayIdentifierToController; +@property(strong, nonatomic) FlutterMethodChannel *methodChannel; +@property(weak, nonatomic) GMSMapView *mapView; @end @implementation FLTTileOverlaysController -- (instancetype)init:(FlutterMethodChannel*)methodChannel - mapView:(GMSMapView*)mapView - registrar:(NSObject*)registrar { +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar { self = [super init]; if (self) { - self.methodChannel = methodChannel; - self.mapView = mapView; - self.tileOverlayIdToController = [[NSMutableDictionary alloc] init]; - self.registrar = registrar; + _methodChannel = methodChannel; + _mapView = mapView; + _tileOverlayIdentifierToController = [[NSMutableDictionary alloc] init]; } return self; } -- (void)addTileOverlays:(NSArray*)tileOverlaysToAdd { - for (NSDictionary* tileOverlay in tileOverlaysToAdd) { - NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; - FLTTileProviderController* tileProvider = - [[FLTTileProviderController alloc] init:self.methodChannel tileOverlayId:tileOverlayId]; - FLTGoogleMapTileOverlayController* controller = +- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd { + for (NSDictionary *tileOverlay in tileOverlaysToAdd) { + NSString *identifier = [FLTTileOverlaysController identifierForTileOverlay:tileOverlay]; + FLTTileProviderController *tileProvider = + [[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]; +- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange { + for (NSDictionary *tileOverlay in tileOverlaysToChange) { + 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/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/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/Classes/FLTGoogleMapsPlugin.m index 7ce2cf1c204d..d62f19ced4f3 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.m +++ b/packages/google_maps_flutter/google_maps_flutter/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]; ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTGoogleMapFactory *googleMapFactory = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; [registrar registerViewFactory:googleMapFactory withId:@"plugins.flutter.io/google_maps" 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/Classes/GoogleMapCircleController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h index 2e7a9967ebd3..6b67760fdaff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h @@ -5,35 +5,23 @@ #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; +@interface FLTGoogleMapCircleController : NSObject - (instancetype)initCircleWithPosition:(CLLocationCoordinate2D)position radius:(CLLocationDistance)radius - circleId:(NSString*)circleId - mapView:(GMSMapView*)mapView; + 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)removeCircleIds:(NSArray*)circleIdsToRemove; -- (void)onCircleTap:(NSString*)circleId; -- (bool)hasCircleWithId:(NSString*)circleId; +- (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/Classes/GoogleMapCircleController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m index bdf36484aaf7..53bf69075c95 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m @@ -3,196 +3,195 @@ // found in the LICENSE file. #import "GoogleMapCircleController.h" -#import "JsonConversions.h" +#import "FLTGoogleMapJSONConversions.h" + +@interface FLTGoogleMapCircleController () + +@property(nonatomic, strong) GMSCircle *circle; +@property(nonatomic, weak) GMSMapView *mapView; + +@end + +@implementation FLTGoogleMapCircleController -@implementation FLTGoogleMapCircleController { - GMSCircle* _circle; - GMSMapView* _mapView; -} - (instancetype)initCircleWithPosition:(CLLocationCoordinate2D)position radius:(CLLocationDistance)radius - circleId:(NSString*)circleId - mapView:(GMSMapView*)mapView { + circleId:(NSString *)circleIdentifier + mapView:(GMSMapView *)mapView + options:(NSDictionary *)options { self = [super init]; if (self) { _circle = [GMSCircle circleWithPosition:position radius:radius]; _mapView = mapView; - _circleId = circleId; - _circle.userData = @[ circleId ]; + _circle.userData = @[ circleIdentifier ]; + [self interpretCircleOptions:options]; } return self; } - (void)removeCircle { - _circle.map = nil; + self.circle.map = nil; } -#pragma mark - FLTGoogleMapCircleOptionsSink methods - - (void)setConsumeTapEvents:(BOOL)consumes { - _circle.tappable = consumes; + self.circle.tappable = consumes; } - (void)setVisible:(BOOL)visible { - _circle.map = visible ? _mapView : nil; + self.circle.map = visible ? self.mapView : nil; } - (void)setZIndex:(int)zIndex { - _circle.zIndex = zIndex; + self.circle.zIndex = zIndex; } - (void)setCenter:(CLLocationCoordinate2D)center { - _circle.position = center; + self.circle.position = center; } - (void)setRadius:(CLLocationDistance)radius { - _circle.radius = radius; + self.circle.radius = radius; } -- (void)setStrokeColor:(UIColor*)color { - _circle.strokeColor = color; +- (void)setStrokeColor:(UIColor *)color { + self.circle.strokeColor = color; } - (void)setStrokeWidth:(CGFloat)width { - _circle.strokeWidth = width; + self.circle.strokeWidth = width; } -- (void)setFillColor:(UIColor*)color { - _circle.fillColor = color; +- (void)setFillColor:(UIColor *)color { + self.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)]; +- (void)interpretCircleOptions:(NSDictionary *)data { + NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents && consumeTapEvents != (id)[NSNull null]) { + [self setConsumeTapEvents:consumeTapEvents.boolValue]; } - NSNumber* visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:ToBool(visible)]; + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; } - NSNumber* zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; } - NSArray* center = data[@"center"]; - if (center) { - [sink setCenter:ToLocation(center)]; + NSArray *center = data[@"center"]; + if (center && center != (id)[NSNull null]) { + [self setCenter:[FLTGoogleMapJSONConversions locationFromLatLong:center]]; } - NSNumber* radius = data[@"radius"]; - if (radius != nil) { - [sink setRadius:ToDistance(radius)]; + NSNumber *radius = data[@"radius"]; + if (radius && radius != (id)[NSNull null]) { + [self setRadius:[radius floatValue]]; } - NSNumber* strokeColor = data[@"strokeColor"]; - if (strokeColor != nil) { - [sink setStrokeColor:ToColor(strokeColor)]; + NSNumber *strokeColor = data[@"strokeColor"]; + if (strokeColor && strokeColor != (id)[NSNull null]) { + [self setStrokeColor:[FLTGoogleMapJSONConversions colorFromRGBA:strokeColor]]; } - NSNumber* strokeWidth = data[@"strokeWidth"]; - if (strokeWidth != nil) { - [sink setStrokeWidth:ToInt(strokeWidth)]; + NSNumber *strokeWidth = data[@"strokeWidth"]; + if (strokeWidth && strokeWidth != (id)[NSNull null]) { + [self setStrokeWidth:[strokeWidth intValue]]; } - NSNumber* fillColor = data[@"fillColor"]; - if (fillColor != nil) { - [sink setFillColor:ToColor(fillColor)]; + NSNumber *fillColor = data[@"fillColor"]; + if (fillColor && fillColor != (id)[NSNull null]) { + [self setFillColor:[FLTGoogleMapJSONConversions colorFromRGBA:fillColor]]; } } -@implementation FLTCirclesController { - NSMutableDictionary* _circleIdToController; - FlutterMethodChannel* _methodChannel; - NSObject* _registrar; - GMSMapView* _mapView; -} -- (instancetype)init:(FlutterMethodChannel*)methodChannel - mapView:(GMSMapView*)mapView - registrar:(NSObject*)registrar { +@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]; - _registrar = registrar; } return self; } -- (void)addCircles:(NSArray*)circlesToAdd { - for (NSDictionary* circle in circlesToAdd) { + +- (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 = + NSString *circleId = [FLTCirclesController getCircleId:circle]; + FLTGoogleMapCircleController *controller = [[FLTGoogleMapCircleController alloc] initCircleWithPosition:position radius:radius circleId:circleId - mapView:_mapView]; - InterpretCircleOptions(circle, controller, _registrar); - _circleIdToController[circleId] = controller; + 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 = _circleIdToController[circleId]; + +- (void)changeCircles:(NSArray *)circlesToChange { + for (NSDictionary *circle in circlesToChange) { + NSString *circleId = [FLTCirclesController getCircleId:circle]; + FLTGoogleMapCircleController *controller = self.circleIdToController[circleId]; if (!controller) { continue; } - InterpretCircleOptions(circle, controller, _registrar); + [controller interpretCircleOptions:circle]; } } -- (void)removeCircleIds:(NSArray*)circleIdsToRemove { - for (NSString* circleId in circleIdsToRemove) { - if (!circleId) { - continue; - } - FLTGoogleMapCircleController* controller = _circleIdToController[circleId]; + +- (void)removeCircleWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapCircleController *controller = self.circleIdToController[identifier]; if (!controller) { continue; } [controller removeCircle]; - [_circleIdToController removeObjectForKey:circleId]; + [self.circleIdToController removeObjectForKey:identifier]; } } -- (bool)hasCircleWithId:(NSString*)circleId { - if (!circleId) { + +- (bool)hasCircleWithIdentifier:(NSString *)identifier { + if (!identifier) { return false; } - return _circleIdToController[circleId] != nil; + return self.circleIdToController[identifier] != nil; } -- (void)onCircleTap:(NSString*)circleId { - if (!circleId) { + +- (void)didTapCircleWithIdentifier:(NSString *)identifier { + if (!identifier) { return; } - FLTGoogleMapCircleController* controller = _circleIdToController[circleId]; + FLTGoogleMapCircleController *controller = self.circleIdToController[identifier]; if (!controller) { return; } - [_methodChannel invokeMethod:@"circle#onTap" arguments:@{@"circleId" : circleId}]; + [self.methodChannel invokeMethod:@"circle#onTap" arguments:@{@"circleId" : identifier}]; } -+ (CLLocationCoordinate2D)getPosition:(NSDictionary*)circle { - NSArray* center = circle[@"center"]; - return ToLocation(center); + ++ (CLLocationCoordinate2D)getPosition:(NSDictionary *)circle { + NSArray *center = circle[@"center"]; + return [FLTGoogleMapJSONConversions locationFromLatLong:center]; } -+ (CLLocationDistance)getRadius:(NSDictionary*)circle { - NSNumber* radius = circle[@"radius"]; - return ToDistance(radius); + ++ (CLLocationDistance)getRadius:(NSDictionary *)circle { + NSNumber *radius = circle[@"radius"]; + return [radius floatValue]; } -+ (NSString*)getCircleId:(NSDictionary*)circle { + ++ (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/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/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/Classes/GoogleMapController.m index be3728753a5d..6378994819dd 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -3,27 +3,20 @@ // 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; + +@end + +@implementation FLTGoogleMapFactory -- (instancetype)initWithRegistrar:(NSObject*)registrar { +- (instancetype)initWithRegistrar:(NSObject *)registrar { self = [super init]; if (self) { _registrar = registrar; @@ -31,62 +24,72 @@ - (instancetype)initWithRegistrar:(NSObject*)registrar { return self; } -- (NSObject*)createArgsCodec { +- (NSObject *)createArgsCodec { return [FlutterStandardMessageCodec sharedInstance]; } -- (NSObject*)createWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args { +- (NSObject *)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { 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; -} +@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 { + 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); - NSString* channelName = + // 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]; _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:registrar.messenger]; __weak __typeof__(self) weakSelf = self; - [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [_channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { if (weakSelf) { [weakSelf onMethodCall:call result:result]; } }]; _mapView.delegate = weakSelf; _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,83 +122,83 @@ - (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; +- (UIView *)view { + return self.mapView; } -- (void)observeValueForKeyPath:(NSString*)keyPath +- (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; + change:(NSDictionary *)change + context:(void *)context { + 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]; } } -- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { +- (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; - GMSCoordinateBounds* bounds = [[GMSCoordinateBounds alloc] initWithRegion:visibleRegion]; - - result(GMSCoordinateBoundsToJson(bounds)); + if (self.mapView != nil) { + GMSVisibleRegion visibleRegion = self.mapView.projection.visibleRegion; + GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:visibleRegion]; + 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 +208,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) { - UIGraphicsImageRendererFormat* format = [UIGraphicsImageRendererFormat defaultFormat]; + if (self.mapView != nil) { + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; format.scale = [[UIScreen mainScreen] scale]; - UIGraphicsImageRenderer* renderer = - [[UIGraphicsImageRenderer alloc] initWithSize:_mapView.frame.size format:format]; + UIGraphicsImageRenderer *renderer = + [[UIGraphicsImageRenderer alloc] initWithSize:self.mapView.frame.size format:format]; - UIImage* image = [renderer imageWithActions:^(UIGraphicsImageRendererContext* context) { - [_mapView.layer renderInContext:context.CGContext]; + UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) { + [self.mapView.layer renderInContext:context.CGContext]; }]; result([FlutterStandardTypedData typedDataWithBytes:UIImagePNGRepresentation(image)]); } else { @@ -227,21 +230,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 +253,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 +262,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,188 +271,186 @@ - (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; + 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; + 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]; - NSString* error = [self setMapStyle:mapStyle]; + NSString *mapStyle = [call arguments]; + NSString *error = [self setMapStyle:mapStyle]; if (error == nil) { result(@[ @(YES) ]); } else { result(@[ @(NO), error ]); } } else if ([call.method isEqualToString:@"map#getTileOverlayInfo"]) { - NSString* rawTileOverlayId = call.arguments[@"tileOverlayId"]; - result([_tileOverlaysController getTileOverlayInfo:rawTileOverlayId]); + NSString *rawTileOverlayId = call.arguments[@"tileOverlayId"]; + 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]; +- (void)animateWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate { + [self.mapView animateWithCameraUpdate:cameraUpdate]; } -- (void)moveWithCameraUpdate:(GMSCameraUpdate*)cameraUpdate { - [_mapView moveCamera:cameraUpdate]; +- (void)moveWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate { + [self.mapView moveCamera:cameraUpdate]; } -- (GMSCameraPosition*)cameraPosition { - if (_trackCameraPosition) { - return _mapView.camera; +- (GMSCameraPosition *)cameraPosition { + if (self.trackCameraPosition) { + return self.mapView.camera; } else { return nil; } } -#pragma mark - FLTGoogleMapOptionsSink methods - -- (void)setCamera:(GMSCameraPosition*)camera { - _mapView.camera = camera; +- (void)setCamera:(GMSCameraPosition *)camera { + self.mapView.camera = camera; } -- (void)setCameraTargetBounds:(GMSCoordinateBounds*)bounds { - _mapView.cameraTargetBounds = bounds; +- (void)setCameraTargetBounds:(GMSCoordinateBounds *)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,255 +458,170 @@ - (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 { +- (NSString *)setMapStyle:(NSString *)mapStyle { if (mapStyle == (id)[NSNull null] || mapStyle.length == 0) { - _mapView.mapStyle = nil; + self.mapView.mapStyle = nil; return nil; } - NSError* error; - GMSMapStyle* style = [GMSMapStyle styleWithJSONString:mapStyle error:&error]; + NSError *error; + GMSMapStyle *style = [GMSMapStyle styleWithJSONString:mapStyle error:&error]; if (!style) { return [error localizedDescription]; } else { - _mapView.mapStyle = style; + self.mapView.mapStyle = style; return nil; } } #pragma mark - GMSMapViewDelegate methods -- (void)mapView:(GMSMapView*)mapView willMove:(BOOL)gesture { - [_channel invokeMethod:@"camera#onMoveStarted" arguments:@{@"isGesture" : @(gesture)}]; +- (void)mapView:(GMSMapView *)mapView willMove:(BOOL)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)}]; +- (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)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:@{}]; -} - -- (BOOL)mapView:(GMSMapView*)mapView didTapMarker:(GMSMarker*)marker { - NSString* markerId = marker.userData[0]; - return [_markersController onMarkerTap:markerId]; -} - -- (void)mapView:(GMSMapView*)mapView didEndDraggingMarker:(GMSMarker*)marker { - NSString* markerId = marker.userData[0]; - [_markersController onMarkerDragEnd:markerId coordinate:marker.position]; -} - -- (void)mapView:(GMSMapView*)mapView didTapInfoWindowOfMarker:(GMSMarker*)marker { - NSString* markerId = marker.userData[0]; - [_markersController onInfoWindowTap: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]; - } +- (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position { + [self.channel invokeMethod:@"camera#onIdle" arguments:@{}]; } -- (void)mapView:(GMSMapView*)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate { - [_channel invokeMethod:@"map#onTap" arguments:@{@"position" : LocationToJson(coordinate)}]; +- (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { + NSString *markerId = marker.userData[0]; + return [self.markersController didTapMarkerWithIdentifier:markerId]; } -- (void)mapView:(GMSMapView*)mapView didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate { - [_channel invokeMethod:@"map#onLongPress" arguments:@{@"position" : LocationToJson(coordinate)}]; +- (void)mapView:(GMSMapView *)mapView didEndDraggingMarker:(GMSMarker *)marker { + NSString *markerId = marker.userData[0]; + [self.markersController didEndDraggingMarkerWithIdentifier:markerId location:marker.position]; } -@end - -#pragma mark - Implementations of JSON conversion functions. - -static NSArray* LocationToJson(CLLocationCoordinate2D position) { - return @[ @(position.latitude), @(position.longitude) ]; +- (void)mapView:(GMSMapView *)mapView didStartDraggingMarker:(GMSMarker *)marker { + NSString *markerId = marker.userData[0]; + [self.markersController didStartDraggingMarkerWithIdentifier:markerId location:marker.position]; } -static NSDictionary* PositionToJson(GMSCameraPosition* position) { - if (!position) { - return nil; - } - return @{ - @"target" : LocationToJson([position target]), - @"zoom" : @([position zoom]), - @"bearing" : @([position bearing]), - @"tilt" : @([position viewingAngle]), - }; +- (void)mapView:(GMSMapView *)mapView didDragMarker:(GMSMarker *)marker { + NSString *markerId = marker.userData[0]; + [self.markersController didDragMarkerWithIdentifier:markerId location:marker.position]; } -static NSDictionary* PointToJson(CGPoint point) { - return @{ - @"x" : @(lroundf(point.x)), - @"y" : @(lroundf(point.y)), - }; +- (void)mapView:(GMSMapView *)mapView didTapInfoWindowOfMarker:(GMSMarker *)marker { + NSString *markerId = marker.userData[0]; + [self.markersController didTapInfoWindowOfMarkerWithIdentifier:markerId]; } - -static NSDictionary* GMSCoordinateBoundsToJson(GMSCoordinateBounds* bounds) { - if (!bounds) { - return nil; +- (void)mapView:(GMSMapView *)mapView didTapOverlay:(GMSOverlay *)overlay { + NSString *overlayId = overlay.userData[0]; + 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]; } - 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]); +- (void)mapView:(GMSMapView *)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate { + [self.channel + invokeMethod:@"map#onTap" + arguments:@{@"position" : [FLTGoogleMapJSONConversions arrayFromLocation:coordinate]}]; } -static GMSMapViewType ToMapViewType(NSNumber* json) { - int value = ToInt(json); - return (GMSMapViewType)(value == 0 ? 5 : value); +- (void)mapView:(GMSMapView *)mapView didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate { + [self.channel + invokeMethod:@"map#onLongPress" + arguments:@{@"position" : [FLTGoogleMapJSONConversions arrayFromLocation:coordinate]}]; } -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])]; +- (void)interpretMapOptions:(NSDictionary *)data { + NSArray *cameraTargetBounds = data[@"cameraTargetBounds"]; + if (cameraTargetBounds && cameraTargetBounds != (id)[NSNull null]) { + [self + setCameraTargetBounds:cameraTargetBounds.count > 0 && cameraTargetBounds[0] != [NSNull null] + ? [FLTGoogleMapJSONConversions + coordinateBoundsFromLatLongs:cameraTargetBounds.firstObject] + : nil]; } - return nil; -} - -static void InterpretMapOptions(NSDictionary* data, id sink) { - NSArray* cameraTargetBounds = data[@"cameraTargetBounds"]; - if (cameraTargetBounds) { - [sink setCameraTargetBounds:ToOptionalBounds(cameraTargetBounds)]; - } - NSNumber* compassEnabled = data[@"compassEnabled"]; - if (compassEnabled != nil) { - [sink setCompassEnabled:ToBool(compassEnabled)]; + NSNumber *compassEnabled = data[@"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]; + NSArray *zoomData = data[@"minMaxZoomPreference"]; + 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"]; + 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)]; + NSNumber *rotateGesturesEnabled = data[@"rotateGesturesEnabled"]; + if (rotateGesturesEnabled && rotateGesturesEnabled != (id)[NSNull null]) { + [self setRotateGesturesEnabled:[rotateGesturesEnabled boolValue]]; } - NSNumber* scrollGesturesEnabled = data[@"scrollGesturesEnabled"]; - if (scrollGesturesEnabled != nil) { - [sink setScrollGesturesEnabled:ToBool(scrollGesturesEnabled)]; + NSNumber *scrollGesturesEnabled = data[@"scrollGesturesEnabled"]; + if (scrollGesturesEnabled && scrollGesturesEnabled != (id)[NSNull null]) { + [self setScrollGesturesEnabled:[scrollGesturesEnabled boolValue]]; } - NSNumber* tiltGesturesEnabled = data[@"tiltGesturesEnabled"]; - if (tiltGesturesEnabled != nil) { - [sink setTiltGesturesEnabled:ToBool(tiltGesturesEnabled)]; + NSNumber *tiltGesturesEnabled = data[@"tiltGesturesEnabled"]; + if (tiltGesturesEnabled && tiltGesturesEnabled != (id)[NSNull null]) { + [self setTiltGesturesEnabled:[tiltGesturesEnabled boolValue]]; } - NSNumber* trackCameraPosition = data[@"trackCameraPosition"]; - if (trackCameraPosition != nil) { - [sink setTrackCameraPosition:ToBool(trackCameraPosition)]; + NSNumber *trackCameraPosition = data[@"trackCameraPosition"]; + if (trackCameraPosition && trackCameraPosition != (id)[NSNull null]) { + [self setTrackCameraPosition:[trackCameraPosition boolValue]]; } - NSNumber* zoomGesturesEnabled = data[@"zoomGesturesEnabled"]; - if (zoomGesturesEnabled != nil) { - [sink setZoomGesturesEnabled:ToBool(zoomGesturesEnabled)]; + NSNumber *zoomGesturesEnabled = data[@"zoomGesturesEnabled"]; + if (zoomGesturesEnabled && zoomGesturesEnabled != (id)[NSNull null]) { + [self setZoomGesturesEnabled:[zoomGesturesEnabled boolValue]]; } - NSNumber* myLocationEnabled = data[@"myLocationEnabled"]; - if (myLocationEnabled != nil) { - [sink setMyLocationEnabled:ToBool(myLocationEnabled)]; + NSNumber *myLocationEnabled = data[@"myLocationEnabled"]; + if (myLocationEnabled && myLocationEnabled != (id)[NSNull null]) { + [self setMyLocationEnabled:[myLocationEnabled boolValue]]; } - NSNumber* myLocationButtonEnabled = data[@"myLocationButtonEnabled"]; - if (myLocationButtonEnabled != nil) { - [sink setMyLocationButtonEnabled:ToBool(myLocationButtonEnabled)]; + NSNumber *myLocationButtonEnabled = data[@"myLocationButtonEnabled"]; + if (myLocationButtonEnabled && myLocationButtonEnabled != (id)[NSNull null]) { + [self setMyLocationButtonEnabled:[myLocationButtonEnabled boolValue]]; } } + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController_Test.h new file mode 100644 index 000000000000..84f6f7ca485f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h index d3e835435ed9..a33d48073dd2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h @@ -8,48 +8,37 @@ 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; +@interface FLTGoogleMapMarkerController : NSObject +@property(assign, nonatomic, readonly) BOOL consumeTapEvents; - (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position - markerId:(NSString*)markerId - mapView:(GMSMapView*)mapView; + identifier:(NSString *)identifier + 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)onMarkerDragEnd:(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; +- (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/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index 6a9fb885afac..dd07e791a888 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m @@ -3,184 +3,159 @@ // found in the LICENSE file. #import "GoogleMapMarkerController.h" -#import "JsonConversions.h" +#import "FLTGoogleMapJSONConversions.h" -static UIImage* ExtractIcon(NSObject* registrar, NSArray* icon); -static void InterpretInfoWindow(id sink, NSDictionary* data); +@interface FLTGoogleMapMarkerController () + +@property(strong, nonatomic) GMSMarker *marker; +@property(weak, nonatomic) GMSMapView *mapView; +@property(assign, nonatomic, readwrite) BOOL consumeTapEvents; + +@end + +@implementation FLTGoogleMapMarkerController -@implementation FLTGoogleMapMarkerController { - GMSMarker* _marker; - GMSMapView* _mapView; - BOOL _consumeTapEvents; -} - (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position - markerId:(NSString*)markerId - mapView:(GMSMapView*)mapView { + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView { self = [super init]; if (self) { _marker = [GMSMarker markerWithPosition:position]; _mapView = mapView; - _markerId = markerId; - _marker.userData = @[ _markerId ]; - _consumeTapEvents = NO; + _marker.userData = @[ identifier ]; } return self; } + - (void)showInfoWindow { - _mapView.selectedMarker = _marker; + self.mapView.selectedMarker = self.marker; } + - (void)hideInfoWindow { - if (_mapView.selectedMarker == _marker) { - _mapView.selectedMarker = nil; + if (self.mapView.selectedMarker == self.marker) { + self.mapView.selectedMarker = nil; } } + - (BOOL)isInfoWindowShown { - return _mapView.selectedMarker == _marker; -} -- (BOOL)consumeTapEvents { - return _consumeTapEvents; + return self.mapView.selectedMarker == self.marker; } + - (void)removeMarker { - _marker.map = nil; + self.marker.map = nil; } -#pragma mark - FLTGoogleMapMarkerOptionsSink methods - - (void)setAlpha:(float)alpha { - _marker.opacity = alpha; + self.marker.opacity = alpha; } + - (void)setAnchor:(CGPoint)anchor { - _marker.groundAnchor = anchor; -} -- (void)setConsumeTapEvents:(BOOL)consumes { - _consumeTapEvents = consumes; + self.marker.groundAnchor = anchor; } + - (void)setDraggable:(BOOL)draggable { - _marker.draggable = draggable; + self.marker.draggable = draggable; } + - (void)setFlat:(BOOL)flat { - _marker.flat = flat; + self.marker.flat = flat; } -- (void)setIcon:(UIImage*)icon { - _marker.icon = icon; + +- (void)setIcon:(UIImage *)icon { + self.marker.icon = icon; } + - (void)setInfoWindowAnchor:(CGPoint)anchor { - _marker.infoWindowAnchor = anchor; + self.marker.infoWindowAnchor = anchor; } -- (void)setInfoWindowTitle:(NSString*)title snippet:(NSString*)snippet { - _marker.title = title; - _marker.snippet = snippet; + +- (void)setInfoWindowTitle:(NSString *)title snippet:(NSString *)snippet { + self.marker.title = title; + self.marker.snippet = snippet; } + - (void)setPosition:(CLLocationCoordinate2D)position { - _marker.position = position; + self.marker.position = position; } + - (void)setRotation:(CLLocationDegrees)rotation { - _marker.rotation = rotation; -} -- (void)setVisible:(BOOL)visible { - _marker.map = visible ? _mapView : nil; + self.marker.rotation = rotation; } -- (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]; +- (void)setVisible:(BOOL)visible { + self.marker.map = visible ? self.mapView : nil; } -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]; +- (void)setZIndex:(int)zIndex { + self.marker.zIndex = zIndex; } -static void InterpretMarkerOptions(NSDictionary* data, id sink, - NSObject* registrar) { - NSNumber* alpha = data[@"alpha"]; - if (alpha != nil) { - [sink setAlpha:ToFloat(alpha)]; +- (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) { - [sink setAnchor:ToPoint(anchor)]; + NSArray *anchor = data[@"anchor"]; + if (anchor && anchor != (id)[NSNull null]) { + [self setAnchor:[FLTGoogleMapJSONConversions pointFromArray:anchor]]; } - NSNumber* draggable = data[@"draggable"]; - if (draggable != nil) { - [sink setDraggable:ToBool(draggable)]; + NSNumber *draggable = data[@"draggable"]; + if (draggable && draggable != (id)[NSNull null]) { + [self setDraggable:[draggable boolValue]]; } - NSArray* icon = data[@"icon"]; - if (icon) { - UIImage* image = ExtractIcon(registrar, icon); - [sink setIcon:image]; + 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 != nil) { - [sink setFlat:ToBool(flat)]; + NSNumber *flat = data[@"flat"]; + if (flat && flat != (id)[NSNull null]) { + [self setFlat:[flat boolValue]]; } - NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents != nil) { - [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; + NSNumber *consumeTapEvents = data[@"consumeTapEvents"]; + if (consumeTapEvents && consumeTapEvents != (id)[NSNull null]) { + [self setConsumeTapEvents:[consumeTapEvents boolValue]]; } - InterpretInfoWindow(sink, data); - NSArray* position = data[@"position"]; - if (position) { - [sink setPosition:ToLocation(position)]; + [self interpretInfoWindow:data]; + NSArray *position = data[@"position"]; + if (position && position != (id)[NSNull null]) { + [self setPosition:[FLTGoogleMapJSONConversions locationFromLatLong:position]]; } - NSNumber* rotation = data[@"rotation"]; - if (rotation != nil) { - [sink setRotation:ToDouble(rotation)]; + NSNumber *rotation = data[@"rotation"]; + if (rotation && rotation != (id)[NSNull null]) { + [self setRotation:[rotation doubleValue]]; } - NSNumber* visible = data[@"visible"]; - if (visible != nil) { - [sink setVisible:ToBool(visible)]; + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; } - NSNumber* zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; } } -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]; +- (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) { - [sink setInfoWindowAnchor:ToPoint(infoWindowAnchor)]; + NSArray *infoWindowAnchor = infoWindow[@"infoWindowAnchor"]; + if (infoWindowAnchor && infoWindowAnchor != (id)[NSNull null]) { + [self setInfoWindowAnchor:[FLTGoogleMapJSONConversions pointFromArray: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; +- (UIImage *)extractIconFromData:(NSArray *)iconData + registrar:(NSObject *)registrar { + UIImage *image; if ([iconData.firstObject isEqualToString:@"defaultMarker"]) { - CGFloat hue = (iconData.count == 1) ? 0.0f : ToDouble(iconData[1]); + CGFloat hue = (iconData.count == 1) ? 0.0f : [iconData[1] doubleValue]; image = [GMSMarker markerImageWithColor:[UIColor colorWithHue:hue / 360.0 saturation:1.0 brightness:0.7 @@ -195,13 +170,13 @@ static void InterpretInfoWindow(id sink, NSDictio } 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); + id scaleParam = iconData[2]; + image = [self scaleImage:image by:scaleParam]; } else { - NSString* error = + NSString *error = [NSString stringWithFormat:@"'fromAssetImage' should have exactly 3 arguments. Got: %lu", (unsigned long)iconData.count]; - NSException* exception = [NSException exceptionWithName:@"InvalidBitmapDescriptor" + NSException *exception = [NSException exceptionWithName:@"InvalidBitmapDescriptor" reason:error userInfo:nil]; @throw exception; @@ -209,19 +184,19 @@ static void InterpretInfoWindow(id sink, NSDictio } else if ([iconData[0] isEqualToString:@"fromBytes"]) { if (iconData.count == 2) { @try { - FlutterStandardTypedData* byteData = iconData[1]; + FlutterStandardTypedData *byteData = iconData[1]; CGFloat screenScale = [[UIScreen mainScreen] scale]; image = [UIImage imageWithData:[byteData data] scale:screenScale]; - } @catch (NSException* exception) { + } @catch (NSException *exception) { @throw [NSException exceptionWithName:@"InvalidByteDescriptor" reason:@"Unable to interpret bytes as a valid image." userInfo:nil]; } } else { - NSString* error = [NSString + NSString *error = [NSString stringWithFormat:@"fromBytes should have exactly one argument, the bytes. Got: %lu", (unsigned long)iconData.count]; - NSException* exception = [NSException exceptionWithName:@"InvalidByteDescriptor" + NSException *exception = [NSException exceptionWithName:@"InvalidByteDescriptor" reason:error userInfo:nil]; @throw exception; @@ -231,88 +206,145 @@ static void InterpretInfoWindow(id sink, NSDictio return image; } -@implementation FLTMarkersController { - NSMutableDictionary* _markerIdToController; - FlutterMethodChannel* _methodChannel; - NSObject* _registrar; - GMSMapView* _mapView; +- (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; } -- (instancetype)init:(FlutterMethodChannel*)methodChannel - mapView:(GMSMapView*)mapView - registrar:(NSObject*)registrar { + +@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; - _markerIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _markerIdentifierToController = [[NSMutableDictionary alloc] init]; _registrar = registrar; } return self; } -- (void)addMarkers:(NSArray*)markersToAdd { - for (NSDictionary* marker in markersToAdd) { + +- (void)addMarkers:(NSArray *)markersToAdd { + for (NSDictionary *marker in markersToAdd) { CLLocationCoordinate2D position = [FLTMarkersController getPosition:marker]; - NSString* markerId = [FLTMarkersController getMarkerId:marker]; - FLTGoogleMapMarkerController* controller = + NSString *identifier = marker[@"markerId"]; + FLTGoogleMapMarkerController *controller = [[FLTGoogleMapMarkerController alloc] initMarkerWithPosition:position - markerId:markerId - mapView:_mapView]; - InterpretMarkerOptions(marker, controller, _registrar); - _markerIdToController[markerId] = controller; + 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* markerId = [FLTMarkersController getMarkerId:marker]; - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + +- (void)changeMarkers:(NSArray *)markersToChange { + for (NSDictionary *marker in markersToChange) { + NSString *identifier = marker[@"markerId"]; + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; if (!controller) { continue; } - InterpretMarkerOptions(marker, controller, _registrar); + [controller interpretMarkerOptions:marker registrar:self.registrar]; } } -- (void)removeMarkerIds:(NSArray*)markerIdsToRemove { - for (NSString* markerId in markerIdsToRemove) { - if (!markerId) { - continue; - } - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + +- (void)removeMarkersWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; if (!controller) { continue; } [controller removeMarker]; - [_markerIdToController removeObjectForKey:markerId]; + [self.markerIdentifierToController removeObjectForKey:identifier]; } } -- (BOOL)onMarkerTap:(NSString*)markerId { - if (!markerId) { + +- (BOOL)didTapMarkerWithIdentifier:(NSString *)identifier { + if (!identifier) { return NO; } - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; if (!controller) { return NO; } - [_methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : markerId}]; + [self.methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : identifier}]; return controller.consumeTapEvents; } -- (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { - if (!markerId) { + +- (void)didStartDraggingMarkerWithIdentifier:(NSString *)identifier + location:(CLLocationCoordinate2D)location { + if (!identifier) { return; } - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; if (!controller) { return; } - [_methodChannel invokeMethod:@"marker#onDragEnd" - arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; + [self.methodChannel invokeMethod:@"marker#onDragStart" + arguments:@{ + @"markerId" : identifier, + @"position" : [FLTGoogleMapJSONConversions arrayFromLocation:location] + }]; } -- (void)onInfoWindowTap:(NSString*)markerId { - if (markerId && _markerIdToController[markerId]) { - [_methodChannel invokeMethod:@"infoWindow#onTap" arguments:@{@"markerId" : markerId}]; + +- (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)showMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result { - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + +- (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); @@ -322,8 +354,9 @@ - (void)showMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result { details:nil]); } } -- (void)hideMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result { - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + +- (void)hideMarkerInfoWindowWithIdentifier:(NSString *)identifier result:(FlutterResult)result { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; if (controller) { [controller hideInfoWindow]; result(nil); @@ -333,8 +366,10 @@ - (void)hideMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result { details:nil]); } } -- (void)isMarkerInfoWindowShown:(NSString*)markerId result:(FlutterResult)result { - FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + +- (void)isInfoWindowShownForMarkerWithIdentifier:(NSString *)identifier + result:(FlutterResult)result { + FLTGoogleMapMarkerController *controller = self.markerIdentifierToController[identifier]; if (controller) { result(@([controller isInfoWindowShown])); } else { @@ -344,11 +379,9 @@ - (void)isMarkerInfoWindowShown:(NSString*)markerId result:(FlutterResult)result } } -+ (CLLocationCoordinate2D)getPosition:(NSDictionary*)marker { - NSArray* position = marker[@"position"]; - return ToLocation(position); -} -+ (NSString*)getMarkerId:(NSDictionary*)marker { - return marker[@"markerId"]; ++ (CLLocationCoordinate2D)getPosition:(NSDictionary *)marker { + NSArray *position = marker[@"position"]; + return [FLTGoogleMapJSONConversions locationFromLatLong:position]; } + @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 index b123ac0a3d68..bd0c9110200e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h @@ -5,34 +5,21 @@ #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; +@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)removePolygonIds:(NSArray*)polygonIdsToRemove; -- (void)onPolygonTap:(NSString*)polygonId; -- (bool)hasPolygonWithId:(NSString*)polygonId; +- (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/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m index 5ad8d4d3bc0e..398adfcacecb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m @@ -3,209 +3,204 @@ // found in the LICENSE file. #import "GoogleMapPolygonController.h" -#import "JsonConversions.h" +#import "FLTGoogleMapJSONConversions.h" -@implementation FLTGoogleMapPolygonController { - GMSPolygon* _polygon; - GMSMapView* _mapView; -} -- (instancetype)initPolygonWithPath:(GMSMutablePath*)path - polygonId:(NSString*)polygonId - mapView:(GMSMapView*)mapView { +@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; - _polygonId = polygonId; - _polygon.userData = @[ polygonId ]; + _polygon.userData = @[ identifier ]; } return self; } - (void)removePolygon { - _polygon.map = nil; + self.polygon.map = nil; } -#pragma mark - FLTGoogleMapPolygonOptionsSink methods - - (void)setConsumeTapEvents:(BOOL)consumes { - _polygon.tappable = consumes; + self.polygon.tappable = consumes; } - (void)setVisible:(BOOL)visible { - _polygon.map = visible ? _mapView : nil; + self.polygon.map = visible ? self.mapView : nil; } - (void)setZIndex:(int)zIndex { - _polygon.zIndex = zIndex; + self.polygon.zIndex = zIndex; } -- (void)setPoints:(NSArray*)points { - GMSMutablePath* path = [GMSMutablePath path]; +- (void)setPoints:(NSArray *)points { + GMSMutablePath *path = [GMSMutablePath path]; - for (CLLocation* location in points) { + for (CLLocation *location in points) { [path addCoordinate:location.coordinate]; } - _polygon.path = path; + self.polygon.path = path; } -- (void)setHoles:(NSArray*>*)rawHoles { - NSMutableArray* holes = [[NSMutableArray alloc] init]; +- (void)setHoles:(NSArray *> *)rawHoles { + NSMutableArray *holes = [[NSMutableArray alloc] init]; - for (NSArray* points in rawHoles) { - GMSMutablePath* path = [GMSMutablePath path]; - for (CLLocation* location in points) { + for (NSArray *points in rawHoles) { + GMSMutablePath *path = [GMSMutablePath path]; + for (CLLocation *location in points) { [path addCoordinate:location.coordinate]; } [holes addObject:path]; } - _polygon.holes = holes; + self.polygon.holes = holes; } -- (void)setFillColor:(UIColor*)color { - _polygon.fillColor = color; +- (void)setFillColor:(UIColor *)color { + self.polygon.fillColor = color; } -- (void)setStrokeColor:(UIColor*)color { - _polygon.strokeColor = color; +- (void)setStrokeColor:(UIColor *)color { + self.polygon.strokeColor = color; } - (void)setStrokeWidth:(CGFloat)width { - _polygon.strokeWidth = width; + self.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)]; +- (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 != nil) { - [sink setVisible:ToBool(visible)]; + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; } - NSNumber* zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; } - NSArray* points = data[@"points"]; - if (points) { - [sink setPoints:ToPoints(points)]; + NSArray *points = data[@"points"]; + if (points && points != (id)[NSNull null]) { + [self setPoints:[FLTGoogleMapJSONConversions pointsFromLatLongs:points]]; } - NSArray* holes = data[@"holes"]; - if (holes) { - [sink setHoles:ToHoles(holes)]; + NSArray *holes = data[@"holes"]; + if (holes && holes != (id)[NSNull null]) { + [self setHoles:[FLTGoogleMapJSONConversions holesFromPointsArray:holes]]; } - NSNumber* fillColor = data[@"fillColor"]; - if (fillColor != nil) { - [sink setFillColor:ToColor(fillColor)]; + NSNumber *fillColor = data[@"fillColor"]; + if (fillColor && fillColor != (id)[NSNull null]) { + [self setFillColor:[FLTGoogleMapJSONConversions colorFromRGBA:fillColor]]; } - NSNumber* strokeColor = data[@"strokeColor"]; - if (strokeColor != nil) { - [sink setStrokeColor:ToColor(strokeColor)]; + NSNumber *strokeColor = data[@"strokeColor"]; + if (strokeColor && strokeColor != (id)[NSNull null]) { + [self setStrokeColor:[FLTGoogleMapJSONConversions colorFromRGBA:strokeColor]]; } - NSNumber* strokeWidth = data[@"strokeWidth"]; - if (strokeWidth != nil) { - [sink setStrokeWidth:ToInt(strokeWidth)]; + NSNumber *strokeWidth = data[@"strokeWidth"]; + if (strokeWidth && strokeWidth != (id)[NSNull null]) { + [self setStrokeWidth:[strokeWidth intValue]]; } } -@implementation FLTPolygonsController { - NSMutableDictionary* _polygonIdToController; - FlutterMethodChannel* _methodChannel; - NSObject* _registrar; - GMSMapView* _mapView; -} -- (instancetype)init:(FlutterMethodChannel*)methodChannel - mapView:(GMSMapView*)mapView - registrar:(NSObject*)registrar { +@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; - _polygonIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _polygonIdentifierToController = [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 = + +- (void)addPolygons:(NSArray *)polygonsToAdd { + for (NSDictionary *polygon in polygonsToAdd) { + GMSMutablePath *path = [FLTPolygonsController getPath:polygon]; + NSString *identifier = polygon[@"polygonId"]; + FLTGoogleMapPolygonController *controller = [[FLTGoogleMapPolygonController alloc] initPolygonWithPath:path - polygonId:polygonId - mapView:_mapView]; - InterpretPolygonOptions(polygon, controller, _registrar); - _polygonIdToController[polygonId] = controller; + 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* polygonId = [FLTPolygonsController getPolygonId:polygon]; - FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId]; + +- (void)changePolygons:(NSArray *)polygonsToChange { + for (NSDictionary *polygon in polygonsToChange) { + NSString *identifier = polygon[@"polygonId"]; + FLTGoogleMapPolygonController *controller = self.polygonIdentifierToController[identifier]; if (!controller) { continue; } - InterpretPolygonOptions(polygon, controller, _registrar); + [controller interpretPolygonOptions:polygon registrar:self.registrar]; } } -- (void)removePolygonIds:(NSArray*)polygonIdsToRemove { - for (NSString* polygonId in polygonIdsToRemove) { - if (!polygonId) { - continue; - } - FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId]; + +- (void)removePolygonWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapPolygonController *controller = self.polygonIdentifierToController[identifier]; if (!controller) { continue; } [controller removePolygon]; - [_polygonIdToController removeObjectForKey:polygonId]; + [self.polygonIdentifierToController removeObjectForKey:identifier]; } } -- (void)onPolygonTap:(NSString*)polygonId { - if (!polygonId) { + +- (void)didTapPolygonWithIdentifier:(NSString *)identifier { + if (!identifier) { return; } - FLTGoogleMapPolygonController* controller = _polygonIdToController[polygonId]; + FLTGoogleMapPolygonController *controller = self.polygonIdentifierToController[identifier]; if (!controller) { return; } - [_methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : polygonId}]; + [self.methodChannel invokeMethod:@"polygon#onTap" arguments:@{@"polygonId" : identifier}]; } -- (bool)hasPolygonWithId:(NSString*)polygonId { - if (!polygonId) { + +- (bool)hasPolygonWithIdentifier:(NSString *)identifier { + if (!identifier) { return false; } - return _polygonIdToController[polygonId] != nil; + return self.polygonIdentifierToController[identifier] != nil; } -+ (GMSMutablePath*)getPath:(NSDictionary*)polygon { - NSArray* pointArray = polygon[@"points"]; - NSArray* points = ToPoints(pointArray); - GMSMutablePath* path = [GMSMutablePath path]; - for (CLLocation* location in points) { + ++ (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; } -+ (NSString*)getPolygonId:(NSDictionary*)polygon { - return polygon[@"polygonId"]; -} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h index f7fafc2f065f..f85d1a3896fa 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.h @@ -5,33 +5,21 @@ #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; -- (instancetype)initPolylineWithPath:(GMSMutablePath*)path - polylineId:(NSString*)polylineId - mapView:(GMSMapView*)mapView; +@interface FLTGoogleMapPolylineController : NSObject +- (instancetype)initPolylineWithPath:(GMSMutablePath *)path + identifier:(NSString *)identifier + mapView:(GMSMapView *)mapView; - (void)removePolyline; @end @interface FLTPolylinesController : NSObject -- (instancetype)init:(FlutterMethodChannel*)methodChannel - mapView:(GMSMapView*)mapView - registrar:(NSObject*)registrar; -- (void)addPolylines:(NSArray*)polylinesToAdd; -- (void)changePolylines:(NSArray*)polylinesToChange; -- (void)removePolylineIds:(NSArray*)polylineIdsToRemove; -- (void)onPolylineTap:(NSString*)polylineId; -- (bool)hasPolylineWithId:(NSString*)polylineId; +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addPolylines:(NSArray *)polylinesToAdd; +- (void)changePolylines:(NSArray *)polylinesToChange; +- (void)removePolylineWithIdentifiers:(NSArray *)identifiers; +- (void)didTapPolylineWithIdentifier:(NSString *)identifier; +- (bool)hasPolylineWithIdentifier:(NSString *)identifier; @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 index 8c70d2c161ba..77601d4a1bb5 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m @@ -3,188 +3,182 @@ // found in the LICENSE file. #import "GoogleMapPolylineController.h" -#import "JsonConversions.h" +#import "FLTGoogleMapJSONConversions.h" -@implementation FLTGoogleMapPolylineController { - GMSPolyline* _polyline; - GMSMapView* _mapView; -} -- (instancetype)initPolylineWithPath:(GMSMutablePath*)path - polylineId:(NSString*)polylineId - mapView:(GMSMapView*)mapView { +@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; - _polylineId = polylineId; - _polyline.userData = @[ polylineId ]; + _polyline.userData = @[ identifier ]; } return self; } - (void)removePolyline { - _polyline.map = nil; + self.polyline.map = nil; } -#pragma mark - FLTGoogleMapPolylineOptionsSink methods - - (void)setConsumeTapEvents:(BOOL)consumes { - _polyline.tappable = consumes; + self.polyline.tappable = consumes; } - (void)setVisible:(BOOL)visible { - _polyline.map = visible ? _mapView : nil; + self.polyline.map = visible ? self.mapView : nil; } - (void)setZIndex:(int)zIndex { - _polyline.zIndex = zIndex; + self.polyline.zIndex = zIndex; } -- (void)setPoints:(NSArray*)points { - GMSMutablePath* path = [GMSMutablePath path]; +- (void)setPoints:(NSArray *)points { + GMSMutablePath *path = [GMSMutablePath path]; - for (CLLocation* location in points) { + for (CLLocation *location in points) { [path addCoordinate:location.coordinate]; } - _polyline.path = path; + self.polyline.path = path; } -- (void)setColor:(UIColor*)color { - _polyline.strokeColor = color; +- (void)setColor:(UIColor *)color { + self.polyline.strokeColor = color; } - (void)setStrokeWidth:(CGFloat)width { - _polyline.strokeWidth = width; + self.polyline.strokeWidth = width; } - (void)setGeodesic:(BOOL)isGeodesic { - _polyline.geodesic = isGeodesic; + self.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)]; +- (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 != nil) { - [sink setVisible:ToBool(visible)]; + NSNumber *visible = data[@"visible"]; + if (visible && visible != (id)[NSNull null]) { + [self setVisible:[visible boolValue]]; } - NSNumber* zIndex = data[@"zIndex"]; - if (zIndex != nil) { - [sink setZIndex:ToInt(zIndex)]; + NSNumber *zIndex = data[@"zIndex"]; + if (zIndex && zIndex != (id)[NSNull null]) { + [self setZIndex:[zIndex intValue]]; } - NSArray* points = data[@"points"]; - if (points) { - [sink setPoints:ToPoints(points)]; + NSArray *points = data[@"points"]; + if (points && points != (id)[NSNull null]) { + [self setPoints:[FLTGoogleMapJSONConversions pointsFromLatLongs:points]]; } - NSNumber* strokeColor = data[@"color"]; - if (strokeColor != nil) { - [sink setColor:ToColor(strokeColor)]; + NSNumber *strokeColor = data[@"color"]; + if (strokeColor && strokeColor != (id)[NSNull null]) { + [self setColor:[FLTGoogleMapJSONConversions colorFromRGBA:strokeColor]]; } - NSNumber* strokeWidth = data[@"width"]; - if (strokeWidth != nil) { - [sink setStrokeWidth:ToInt(strokeWidth)]; + NSNumber *strokeWidth = data[@"width"]; + if (strokeWidth && strokeWidth != (id)[NSNull null]) { + [self setStrokeWidth:[strokeWidth intValue]]; } - NSNumber* geodesic = data[@"geodesic"]; - if (geodesic != nil) { - [sink setGeodesic:geodesic.boolValue]; + NSNumber *geodesic = data[@"geodesic"]; + if (geodesic && geodesic != (id)[NSNull null]) { + [self setGeodesic:geodesic.boolValue]; } } -@implementation FLTPolylinesController { - NSMutableDictionary* _polylineIdToController; - FlutterMethodChannel* _methodChannel; - NSObject* _registrar; - GMSMapView* _mapView; -} -- (instancetype)init:(FlutterMethodChannel*)methodChannel - mapView:(GMSMapView*)mapView - registrar:(NSObject*)registrar { +@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; - _polylineIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _polylineIdentifierToController = [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 = +- (void)addPolylines:(NSArray *)polylinesToAdd { + for (NSDictionary *polyline in polylinesToAdd) { + GMSMutablePath *path = [FLTPolylinesController getPath:polyline]; + NSString *identifier = polyline[@"polylineId"]; + FLTGoogleMapPolylineController *controller = [[FLTGoogleMapPolylineController alloc] initPolylineWithPath:path - polylineId:polylineId - mapView:_mapView]; - InterpretPolylineOptions(polyline, controller, _registrar); - _polylineIdToController[polylineId] = controller; + 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* polylineId = [FLTPolylinesController getPolylineId:polyline]; - FLTGoogleMapPolylineController* controller = _polylineIdToController[polylineId]; +- (void)changePolylines:(NSArray *)polylinesToChange { + for (NSDictionary *polyline in polylinesToChange) { + NSString *identifier = polyline[@"polylineId"]; + FLTGoogleMapPolylineController *controller = self.polylineIdentifierToController[identifier]; if (!controller) { continue; } - InterpretPolylineOptions(polyline, controller, _registrar); + [controller interpretPolylineOptions:polyline registrar:self.registrar]; } } -- (void)removePolylineIds:(NSArray*)polylineIdsToRemove { - for (NSString* polylineId in polylineIdsToRemove) { - if (!polylineId) { - continue; - } - FLTGoogleMapPolylineController* controller = _polylineIdToController[polylineId]; +- (void)removePolylineWithIdentifiers:(NSArray *)identifiers { + for (NSString *identifier in identifiers) { + FLTGoogleMapPolylineController *controller = self.polylineIdentifierToController[identifier]; if (!controller) { continue; } [controller removePolyline]; - [_polylineIdToController removeObjectForKey:polylineId]; + [self.polylineIdentifierToController removeObjectForKey:identifier]; } } -- (void)onPolylineTap:(NSString*)polylineId { - if (!polylineId) { +- (void)didTapPolylineWithIdentifier:(NSString *)identifier { + if (!identifier) { return; } - FLTGoogleMapPolylineController* controller = _polylineIdToController[polylineId]; + FLTGoogleMapPolylineController *controller = self.polylineIdentifierToController[identifier]; if (!controller) { return; } - [_methodChannel invokeMethod:@"polyline#onTap" arguments:@{@"polylineId" : polylineId}]; + [self.methodChannel invokeMethod:@"polyline#onTap" arguments:@{@"polylineId" : identifier}]; } -- (bool)hasPolylineWithId:(NSString*)polylineId { - if (!polylineId) { +- (bool)hasPolylineWithIdentifier:(NSString *)identifier { + if (!identifier) { return false; } - return _polylineIdToController[polylineId] != nil; + return self.polylineIdentifierToController[identifier] != nil; } -+ (GMSMutablePath*)getPath:(NSDictionary*)polyline { - NSArray* pointArray = polyline[@"points"]; - NSArray* points = ToPoints(pointArray); - GMSMutablePath* path = [GMSMutablePath path]; - for (CLLocation* location in points) { ++ (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; } -+ (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 6ede4dbaf8b4..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 592d7e825b38..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/ios/Classes/google_maps_flutter-umbrella.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-umbrella.h new file mode 100644 index 000000000000..9969e716c26b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter-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/Classes/google_maps_flutter.modulemap b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap new file mode 100644 index 000000000000..19513f4a7602 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/google_maps_flutter.modulemap @@ -0,0 +1,10 @@ +framework module google_maps_flutter { + umbrella header "google_maps_flutter-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/google_maps_flutter.podspec index 35e4f3faf871..e34919c30484 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec +++ b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec @@ -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/master/packages/google_maps_flutter/google_maps_flutter' } + 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_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/google_maps_flutter.modulemap' s.dependency 'Flutter' s.dependency 'GoogleMaps' s.static_framework = true 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 93bb0566dd1f..4eeb8572413c 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,15 +6,15 @@ 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_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' show 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 ba18c5ffc17b..71b1434eb293 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,21 +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: library_private_types_in_public_api + part of google_maps_flutter; /// Controller for a single GoogleMap instance running on the host platform. class GoogleMapController { - /// The mapId for this controller - final int mapId; - GoogleMapController._( - CameraPosition initialCameraPosition, this._googleMapState, { required this.mapId, }) { _connectStreams(mapId); } + /// The mapId for this controller + final int mapId; + /// Initialize control of a [GoogleMap] with [id]. /// /// Mainly for internal use when instantiating a [GoogleMapController] passed @@ -29,7 +30,6 @@ class GoogleMapController { assert(id != null); await GoogleMapsFlutterPlatform.instance.init(id); return GoogleMapController._( - initialCameraPosition, googleMapState, mapId: id, ); @@ -38,7 +38,7 @@ class GoogleMapController { /// Used to communicate with the native platform. /// /// Accessible only for testing. - // TODO(dit) https://github.com/flutter/flutter/issues/55504 Remove this getter. + // TODO(dit): Remove this getter, https://github.com/flutter/flutter/issues/55504. @visibleForTesting MethodChannel? get channel { if (GoogleMapsFlutterPlatform.instance is MethodChannelGoogleMapsFlutter) { @@ -69,6 +69,12 @@ class GoogleMapController { 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)); @@ -96,10 +102,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 26b9d6b83c84..b76d103a99f4 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 @@ -8,7 +8,7 @@ part of google_maps_flutter; /// /// Pass to [GoogleMap.onMapCreated] to receive a [GoogleMapController] when the /// map is created. -typedef void MapCreatedCallback(GoogleMapController controller); +typedef MapCreatedCallback = void Function(GoogleMapController controller); // This counter is used to provide a stable "constant" initialization id // to the buildView function, so the web implementation can use it as a @@ -25,11 +25,12 @@ class UnknownMapObjectIdError extends Error { final String objectType; /// The unknown maps object ID. - final MapsObjectId objectId; + final MapsObjectId objectId; /// The context where the error occurred. final String? context; + @override String toString() { if (context != null) { return 'Unknown $objectType ID "${objectId.value}" in $context'; @@ -38,6 +39,46 @@ class UnknownMapObjectIdError extends Error { } } +/// Android specific settings for [GoogleMap]. +class AndroidGoogleMapsFlutter { + AndroidGoogleMapsFlutter._(); + + /// Whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// Defaults to false. + static bool get useAndroidViewSurface { + final GoogleMapsFlutterPlatform platform = + GoogleMapsFlutterPlatform.instance; + if (platform is MethodChannelGoogleMapsFlutter) { + return platform.useAndroidViewSurface; + } + return false; + } + + /// Set whether to render [GoogleMap] with a [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// Defaults to false. + static set useAndroidViewSurface(bool useAndroidViewSurface) { + final GoogleMapsFlutterPlatform platform = + GoogleMapsFlutterPlatform.instance; + if (platform is MethodChannelGoogleMapsFlutter) { + platform.useAndroidViewSurface = useAndroidViewSurface; + } + } +} + /// A widget which displays a map with data obtained from the Google Maps service. class GoogleMap extends StatefulWidget { /// Creates a widget displaying data from Google Maps services. @@ -61,6 +102,7 @@ class GoogleMap extends StatefulWidget { this.tiltGesturesEnabled = true, this.myLocationEnabled = false, this.myLocationButtonEnabled = true, + this.layoutDirection, /// If no padding is specified default padding will be 0. this.padding = const EdgeInsets.all(0), @@ -100,6 +142,12 @@ class GoogleMap extends StatefulWidget { /// Type of map tiles to be rendered. final MapType mapType; + /// The layout direction to use for the embedded view. + /// + /// If this is null, the ambient [Directionality] is used instead. If there is + /// no ambient [Directionality], [TextDirection.ltr] is used. + final TextDirection? layoutDirection; + /// Preferred bounds for the camera zoom level. /// /// Actual bounds depend on map data and device. @@ -237,7 +285,7 @@ class GoogleMap extends StatefulWidget { } class _GoogleMapState extends State { - final _mapId = _nextMapCreationId++; + final int _mapId = _nextMapCreationId++; final Completer _controller = Completer(); @@ -246,27 +294,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.buildView( + return GoogleMapsFlutterPlatform.instance.buildViewWithConfiguration( _mapId, onPlatformViewCreated, - 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); @@ -274,9 +329,13 @@ class _GoogleMapState extends State { } @override - void dispose() async { + void dispose() { + _disposeController(); super.dispose(); - GoogleMapController controller = await _controller.future; + } + + Future _disposeController() async { + final GoogleMapController controller = await _controller.future; controller.dispose(); } @@ -291,20 +350,19 @@ class _GoogleMapState extends State { _updateTileOverlays(); } - void _updateOptions() async { - final _GoogleMapOptions newOptions = _GoogleMapOptions.fromWidget(widget); - final Map updates = - _googleMapOptions.updatesMap(newOptions); + Future _updateOptions() async { + 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; } - void _updateMarkers() async { + Future _updateMarkers() async { final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures controller._updateMarkers( @@ -312,7 +370,7 @@ class _GoogleMapState extends State { _markers = keyByMarkerId(widget.markers); } - void _updatePolygons() async { + Future _updatePolygons() async { final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures controller._updatePolygons( @@ -320,7 +378,7 @@ class _GoogleMapState extends State { _polygons = keyByPolygonId(widget.polygons); } - void _updatePolylines() async { + Future _updatePolylines() async { final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures controller._updatePolylines( @@ -328,7 +386,7 @@ class _GoogleMapState extends State { _polylines = keyByPolylineId(widget.polylines); } - void _updateCircles() async { + Future _updateCircles() async { final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures controller._updateCircles( @@ -336,7 +394,7 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } - void _updateTileOverlays() async { + Future _updateTileOverlays() async { final GoogleMapController controller = await _controller.future; // ignore: unawaited_futures controller._updateTileOverlays(widget.tileOverlays); @@ -368,6 +426,30 @@ class _GoogleMapState extends State { } } + void onMarkerDragStart(MarkerId markerId, LatLng position) { + assert(markerId != null); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDragStart'); + } + final ValueChanged? onDragStart = marker.onDragStart; + if (onDragStart != null) { + onDragStart(position); + } + } + + void onMarkerDrag(MarkerId markerId, LatLng position) { + assert(markerId != null); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDrag'); + } + final ValueChanged? onDrag = marker.onDrag; + if (onDrag != null) { + onDrag(position); + } + } + void onMarkerDragEnd(MarkerId markerId, LatLng position) { assert(markerId != null); final Marker? marker = _markers[markerId]; @@ -445,98 +527,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 641e475a56f0..1a0dd4ebfc6f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -1,12 +1,12 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. -repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter +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.0.10 +version: 2.1.8 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: @@ -21,17 +21,12 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - google_maps_flutter_platform_interface: ^2.0.0 + google_maps_flutter_platform_interface: ^2.2.0 dev_dependencies: flutter_test: sdk: flutter - # TODO(iskakaushik): The following dependencies can be removed once - # https://github.com/dart-lang/pub/issues/2101 is resolved. - flutter_driver: - sdk: flutter - test: ^1.16.0 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/circle_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart index e0d1180a0abb..6d650661c5e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart @@ -35,7 +35,7 @@ void main() { }); testWidgets('Initializing a circle', (WidgetTester tester) async { - final Circle c1 = Circle(circleId: CircleId("circle_1")); + const Circle c1 = Circle(circleId: CircleId('circle_1')); await tester.pumpWidget(_mapWithCircles({c1})); final FakePlatformGoogleMap platformGoogleMap = @@ -48,9 +48,9 @@ void main() { expect(platformGoogleMap.circlesToChange.isEmpty, true); }); - testWidgets("Adding a circle", (WidgetTester tester) async { - final Circle c1 = Circle(circleId: CircleId("circle_1")); - final Circle c2 = Circle(circleId: CircleId("circle_2")); + testWidgets('Adding a circle', (WidgetTester tester) async { + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_2')); await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c1, c2})); @@ -67,8 +67,8 @@ void main() { expect(platformGoogleMap.circlesToChange.isEmpty, true); }); - testWidgets("Removing a circle", (WidgetTester tester) async { - final Circle c1 = Circle(circleId: CircleId("circle_1")); + testWidgets('Removing a circle', (WidgetTester tester) async { + const Circle c1 = Circle(circleId: CircleId('circle_1')); await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({})); @@ -82,9 +82,9 @@ void main() { expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); - testWidgets("Updating a circle", (WidgetTester tester) async { - final Circle c1 = Circle(circleId: CircleId("circle_1")); - final Circle c2 = Circle(circleId: CircleId("circle_1"), radius: 10); + testWidgets('Updating a circle', (WidgetTester tester) async { + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_1'), radius: 10); await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c2})); @@ -98,9 +98,9 @@ void main() { expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); - testWidgets("Updating a circle", (WidgetTester tester) async { - final Circle c1 = Circle(circleId: CircleId("circle_1")); - final Circle c2 = Circle(circleId: CircleId("circle_1"), radius: 10); + testWidgets('Updating a circle', (WidgetTester tester) async { + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_1'), radius: 10); await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c2})); @@ -114,12 +114,12 @@ void main() { expect(update.radius, 10); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Circle c1 = Circle(circleId: CircleId("circle_1")); - Circle c2 = Circle(circleId: CircleId("circle_2")); + testWidgets('Multi Update', (WidgetTester tester) async { + Circle c1 = const Circle(circleId: CircleId('circle_1')); + Circle c2 = const Circle(circleId: CircleId('circle_2')); final Set prev = {c1, c2}; - c1 = Circle(circleId: CircleId("circle_1"), visible: false); - c2 = Circle(circleId: CircleId("circle_2"), radius: 10); + c1 = const Circle(circleId: CircleId('circle_1'), visible: false); + c2 = const Circle(circleId: CircleId('circle_2'), radius: 10); final Set cur = {c1, c2}; await tester.pumpWidget(_mapWithCircles(prev)); @@ -133,14 +133,14 @@ void main() { expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Circle c2 = Circle(circleId: CircleId("circle_2")); - final Circle c3 = Circle(circleId: CircleId("circle_3")); + testWidgets('Multi Update', (WidgetTester tester) async { + Circle c2 = const Circle(circleId: CircleId('circle_2')); + const Circle c3 = Circle(circleId: CircleId('circle_3')); final Set prev = {c2, c3}; // c1 is added, c2 is updated, c3 is removed. - final Circle c1 = Circle(circleId: CircleId("circle_1")); - c2 = Circle(circleId: CircleId("circle_2"), radius: 10); + const Circle c1 = Circle(circleId: CircleId('circle_1')); + c2 = const Circle(circleId: CircleId('circle_2'), radius: 10); final Set cur = {c1, c2}; await tester.pumpWidget(_mapWithCircles(prev)); @@ -158,12 +158,12 @@ void main() { expect(platformGoogleMap.circleIdsToRemove.first, equals(c3.circleId)); }); - testWidgets("Partial Update", (WidgetTester tester) async { - final Circle c1 = Circle(circleId: CircleId("circle_1")); - final Circle c2 = Circle(circleId: CircleId("circle_2")); - Circle c3 = Circle(circleId: CircleId("circle_3")); + testWidgets('Partial Update', (WidgetTester tester) async { + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_2')); + Circle c3 = const Circle(circleId: CircleId('circle_3')); final Set prev = {c1, c2, c3}; - c3 = Circle(circleId: CircleId("circle_3"), radius: 10); + c3 = const Circle(circleId: CircleId('circle_3'), radius: 10); final Set cur = {c1, c2, c3}; await tester.pumpWidget(_mapWithCircles(prev)); @@ -177,10 +177,11 @@ void main() { expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); - testWidgets("Update non platform related attr", (WidgetTester tester) async { - Circle c1 = Circle(circleId: CircleId("circle_1")); + testWidgets('Update non platform related attr', (WidgetTester tester) async { + Circle c1 = const Circle(circleId: CircleId('circle_1')); final Set prev = {c1}; - c1 = Circle(circleId: CircleId("circle_1"), onTap: () => print("hello")); + c1 = Circle( + circleId: const CircleId('circle_1'), onTap: () => print('hello')); final Set cur = {c1}; await tester.pumpWidget(_mapWithCircles(prev)); 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 37270ea34d29..bac3ceabc4de 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 @@ -15,7 +15,7 @@ class FakePlatformGoogleMap { channel = MethodChannel( 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()) { channel.setMockMethodCallHandler(onMethodCall); - updateOptions(params['options']); + updateOptions(params['options'] as Map); updateMarkers(params); updatePolygons(params); updatePolylines(params); @@ -94,23 +94,23 @@ class FakePlatformGoogleMap { Future onMethodCall(MethodCall call) { switch (call.method) { case 'map#update': - updateOptions(call.arguments['options']); + updateOptions(call.arguments['options'] as Map); return Future.sync(() {}); case 'markers#update': - updateMarkers(call.arguments); + updateMarkers(call.arguments as Map?); return Future.sync(() {}); case 'polygons#update': - updatePolygons(call.arguments); + updatePolygons(call.arguments as Map?); return Future.sync(() {}); case 'polylines#update': - updatePolylines(call.arguments); + updatePolylines(call.arguments as Map?); return Future.sync(() {}); case 'tileOverlays#update': - updateTileOverlays( - Map.castFrom(call.arguments)); + updateTileOverlays(Map.castFrom( + call.arguments as Map)); return Future.sync(() {}); case 'circles#update': - updateCircles(call.arguments); + updateCircles(call.arguments as Map?); return Future.sync(() {}); default: return Future.sync(() {}); @@ -122,8 +122,8 @@ class FakePlatformGoogleMap { return; } markersToAdd = _deserializeMarkers(markerUpdates['markersToAdd']); - markerIdsToRemove = - _deserializeMarkerIds(markerUpdates['markerIdsToRemove']); + markerIdsToRemove = _deserializeMarkerIds( + markerUpdates['markerIdsToRemove'] as List?); markersToChange = _deserializeMarkers(markerUpdates['markersToChange']); } @@ -131,29 +131,32 @@ class FakePlatformGoogleMap { if (markerIds == null) { return {}; } - return markerIds.map((dynamic markerId) => MarkerId(markerId)).toSet(); + return markerIds + .map((dynamic markerId) => MarkerId(markerId as String)) + .toSet(); } Set _deserializeMarkers(dynamic markers) { if (markers == null) { return {}; } - final List markersData = markers; + final List markersData = markers as List; final Set result = {}; - for (Map markerData + for (final Map markerData in markersData.cast>()) { - final String markerId = markerData['markerId']; - final double alpha = markerData['alpha']; - final bool draggable = markerData['draggable']; - final bool visible = markerData['visible']; + final String markerId = markerData['markerId'] as String; + final double alpha = markerData['alpha'] as double; + final bool draggable = markerData['draggable'] as bool; + final bool visible = markerData['visible'] as bool; final dynamic infoWindowData = markerData['infoWindow']; InfoWindow infoWindow = InfoWindow.noText; if (infoWindowData != null) { - final Map infoWindowMap = infoWindowData; + final Map infoWindowMap = + infoWindowData as Map; infoWindow = InfoWindow( - title: infoWindowMap['title'], - snippet: infoWindowMap['snippet'], + title: infoWindowMap['title'] as String?, + snippet: infoWindowMap['snippet'] as String?, ); } @@ -174,8 +177,8 @@ class FakePlatformGoogleMap { return; } polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']); - polygonIdsToRemove = - _deserializePolygonIds(polygonUpdates['polygonIdsToRemove']); + polygonIdsToRemove = _deserializePolygonIds( + polygonUpdates['polygonIdsToRemove'] as List?); polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); } @@ -183,22 +186,26 @@ class FakePlatformGoogleMap { if (polygonIds == null) { return {}; } - return polygonIds.map((dynamic polygonId) => PolygonId(polygonId)).toSet(); + return polygonIds + .map((dynamic polygonId) => PolygonId(polygonId as String)) + .toSet(); } Set _deserializePolygons(dynamic polygons) { if (polygons == null) { return {}; } - final List polygonsData = polygons; + final List polygonsData = polygons as List; final Set result = {}; - for (Map polygonData + for (final Map polygonData in polygonsData.cast>()) { - final String polygonId = polygonData['polygonId']; - final bool visible = polygonData['visible']; - final bool geodesic = polygonData['geodesic']; - final List points = _deserializePoints(polygonData['points']); - final List> holes = _deserializeHoles(polygonData['holes']); + final String polygonId = polygonData['polygonId'] as String; + final bool visible = polygonData['visible'] as bool; + final bool geodesic = polygonData['geodesic'] as bool; + final List points = + _deserializePoints(polygonData['points'] as List); + final List> holes = + _deserializeHoles(polygonData['holes'] as List); result.add(Polygon( polygonId: PolygonId(polygonId), @@ -214,15 +221,15 @@ class FakePlatformGoogleMap { List _deserializePoints(List points) { return points.map((dynamic list) { - return LatLng(list[0], list[1]); + return LatLng(list[0] as double, list[1] as double); }).toList(); } List> _deserializeHoles(List holes) { return holes.map>((dynamic hole) { return hole.map((dynamic list) { - return LatLng(list[0], list[1]); - }).toList(); + return LatLng(list[0] as double, list[1] as double); + }).toList() as List; }).toList(); } @@ -231,8 +238,8 @@ class FakePlatformGoogleMap { return; } polylinesToAdd = _deserializePolylines(polylineUpdates['polylinesToAdd']); - polylineIdsToRemove = - _deserializePolylineIds(polylineUpdates['polylineIdsToRemove']); + polylineIdsToRemove = _deserializePolylineIds( + polylineUpdates['polylineIdsToRemove'] as List?); polylinesToChange = _deserializePolylines(polylineUpdates['polylinesToChange']); } @@ -242,7 +249,7 @@ class FakePlatformGoogleMap { return {}; } return polylineIds - .map((dynamic polylineId) => PolylineId(polylineId)) + .map((dynamic polylineId) => PolylineId(polylineId as String)) .toSet(); } @@ -250,14 +257,15 @@ class FakePlatformGoogleMap { if (polylines == null) { return {}; } - final List polylinesData = polylines; + final List polylinesData = polylines as List; final Set result = {}; - for (Map polylineData + for (final Map polylineData in polylinesData.cast>()) { - final String polylineId = polylineData['polylineId']; - final bool visible = polylineData['visible']; - final bool geodesic = polylineData['geodesic']; - final List points = _deserializePoints(polylineData['points']); + final String polylineId = polylineData['polylineId'] as String; + final bool visible = polylineData['visible'] as bool; + final bool geodesic = polylineData['geodesic'] as bool; + final List points = + _deserializePoints(polylineData['points'] as List); result.add(Polyline( polylineId: PolylineId(polylineId), @@ -275,8 +283,8 @@ class FakePlatformGoogleMap { return; } circlesToAdd = _deserializeCircles(circleUpdates['circlesToAdd']); - circleIdsToRemove = - _deserializeCircleIds(circleUpdates['circleIdsToRemove']); + circleIdsToRemove = _deserializeCircleIds( + circleUpdates['circleIdsToRemove'] as List?); circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); } @@ -287,17 +295,19 @@ class FakePlatformGoogleMap { final List>? tileOverlaysToAddList = updateTileOverlayUpdates['tileOverlaysToAdd'] != null ? List.castFrom>( - updateTileOverlayUpdates['tileOverlaysToAdd']) + updateTileOverlayUpdates['tileOverlaysToAdd'] as List) : null; final List? tileOverlayIdsToRemoveList = updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null ? List.castFrom( - updateTileOverlayUpdates['tileOverlayIdsToRemove']) + updateTileOverlayUpdates['tileOverlayIdsToRemove'] + as List) : null; final List>? tileOverlaysToChangeList = updateTileOverlayUpdates['tileOverlaysToChange'] != null ? List.castFrom>( - updateTileOverlayUpdates['tileOverlaysToChange']) + updateTileOverlayUpdates['tileOverlaysToChange'] + as List) : null; tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList); tileOverlayIdsToRemove = @@ -309,20 +319,22 @@ class FakePlatformGoogleMap { if (circleIds == null) { return {}; } - return circleIds.map((dynamic circleId) => CircleId(circleId)).toSet(); + return circleIds + .map((dynamic circleId) => CircleId(circleId as String)) + .toSet(); } Set _deserializeCircles(dynamic circles) { if (circles == null) { return {}; } - final List circlesData = circles; + final List circlesData = circles as List; final Set result = {}; - for (Map circleData + for (final Map circleData in circlesData.cast>()) { - final String circleId = circleData['circleId']; - final bool visible = circleData['visible']; - final double radius = circleData['radius']; + final String circleId = circleData['circleId'] as String; + final bool visible = circleData['visible'] as bool; + final double radius = circleData['radius'] as double; result.add(Circle( circleId: CircleId(circleId), @@ -349,12 +361,12 @@ class FakePlatformGoogleMap { return {}; } final Set result = {}; - for (Map tileOverlayData in tileOverlays) { - final String tileOverlayId = tileOverlayData['tileOverlayId']; - final bool fadeIn = tileOverlayData['fadeIn']; - final double transparency = tileOverlayData['transparency']; - final int zIndex = tileOverlayData['zIndex']; - final bool visible = tileOverlayData['visible']; + for (final Map tileOverlayData in tileOverlays) { + final String tileOverlayId = tileOverlayData['tileOverlayId'] as String; + final bool fadeIn = tileOverlayData['fadeIn'] as bool; + final double transparency = tileOverlayData['transparency'] as double; + final int zIndex = tileOverlayData['zIndex'] as int; + final bool visible = tileOverlayData['visible'] as bool; result.add(TileOverlay( tileOverlayId: TileOverlayId(tileOverlayId), @@ -370,60 +382,62 @@ class FakePlatformGoogleMap { void updateOptions(Map options) { if (options.containsKey('compassEnabled')) { - compassEnabled = options['compassEnabled']; + compassEnabled = options['compassEnabled'] as bool?; } if (options.containsKey('mapToolbarEnabled')) { - mapToolbarEnabled = options['mapToolbarEnabled']; + mapToolbarEnabled = options['mapToolbarEnabled'] as bool?; } if (options.containsKey('cameraTargetBounds')) { - final List boundsList = options['cameraTargetBounds']; + final List boundsList = + options['cameraTargetBounds'] as List; cameraTargetBounds = boundsList[0] == null ? CameraTargetBounds.unbounded : CameraTargetBounds(LatLngBounds.fromList(boundsList[0])); } if (options.containsKey('mapType')) { - mapType = MapType.values[options['mapType']]; + mapType = MapType.values[options['mapType'] as int]; } if (options.containsKey('minMaxZoomPreference')) { - final List minMaxZoomList = options['minMaxZoomPreference']; - minMaxZoomPreference = - MinMaxZoomPreference(minMaxZoomList[0], minMaxZoomList[1]); + final List minMaxZoomList = + options['minMaxZoomPreference'] as List; + minMaxZoomPreference = MinMaxZoomPreference( + minMaxZoomList[0] as double?, minMaxZoomList[1] as double?); } if (options.containsKey('rotateGesturesEnabled')) { - rotateGesturesEnabled = options['rotateGesturesEnabled']; + rotateGesturesEnabled = options['rotateGesturesEnabled'] as bool?; } if (options.containsKey('scrollGesturesEnabled')) { - scrollGesturesEnabled = options['scrollGesturesEnabled']; + scrollGesturesEnabled = options['scrollGesturesEnabled'] as bool?; } if (options.containsKey('tiltGesturesEnabled')) { - tiltGesturesEnabled = options['tiltGesturesEnabled']; + tiltGesturesEnabled = options['tiltGesturesEnabled'] as bool?; } if (options.containsKey('trackCameraPosition')) { - trackCameraPosition = options['trackCameraPosition']; + trackCameraPosition = options['trackCameraPosition'] as bool?; } if (options.containsKey('zoomGesturesEnabled')) { - zoomGesturesEnabled = options['zoomGesturesEnabled']; + zoomGesturesEnabled = options['zoomGesturesEnabled'] as bool?; } if (options.containsKey('zoomControlsEnabled')) { - zoomControlsEnabled = options['zoomControlsEnabled']; + zoomControlsEnabled = options['zoomControlsEnabled'] as bool?; } if (options.containsKey('liteModeEnabled')) { - liteModeEnabled = options['liteModeEnabled']; + liteModeEnabled = options['liteModeEnabled'] as bool?; } if (options.containsKey('myLocationEnabled')) { - myLocationEnabled = options['myLocationEnabled']; + myLocationEnabled = options['myLocationEnabled'] as bool?; } if (options.containsKey('myLocationButtonEnabled')) { - myLocationButtonEnabled = options['myLocationButtonEnabled']; + myLocationButtonEnabled = options['myLocationButtonEnabled'] as bool?; } if (options.containsKey('trafficEnabled')) { - trafficEnabled = options['trafficEnabled']; + trafficEnabled = options['trafficEnabled'] as bool?; } if (options.containsKey('buildingsEnabled')) { - buildingsEnabled = options['buildingsEnabled']; + buildingsEnabled = options['buildingsEnabled'] as bool?; } if (options.containsKey('padding')) { - padding = options['padding']; + padding = options['padding'] as List?; } } } @@ -434,10 +448,12 @@ class FakePlatformViewsController { Future fakePlatformViewsMethodHandler(MethodCall call) { switch (call.method) { case 'create': - final Map args = call.arguments; - final Map params = _decodeParams(args['params'])!; + final Map args = + call.arguments as Map; + final Map params = + _decodeParams(args['params'] as Uint8List)!; lastCreatedView = FakePlatformGoogleMap( - args['id'], + args['id'] as int, params, ); return Future.sync(() => 1); @@ -457,5 +473,6 @@ Map? _decodeParams(Uint8List paramsMessage) { paramsMessage.offsetInBytes, paramsMessage.lengthInBytes, ); - return const StandardMessageCodec().decodeMessage(messageBytes); + return const StandardMessageCodec().decodeMessage(messageBytes) + as Map?; } 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 d1ec87a4730d..003ae06d9877 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,6 +6,7 @@ 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'; @@ -604,17 +605,21 @@ void main() { }, ); - // TODO(bparrishMines): Uncomment once https://github.com/flutter/plugins/pull/4017 has landed. - // testWidgets('Use AndroidViewSurface on Android', (WidgetTester tester) async { - // await tester.pumpWidget( - // const Directionality( - // textDirection: TextDirection.ltr, - // child: GoogleMap( - // initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - // ), - // ), - // ); - // - // expect(find.byType(AndroidViewSurface), 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 6e0f5ed3e4f5..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'; @@ -32,7 +32,7 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: Column( - children: const [ + children: const [ GoogleMap( initialCameraPosition: CameraPosition( target: LatLng(43.362, -5.849), @@ -57,7 +57,7 @@ void main() { testWidgets('Calls platform.dispose when GoogleMap is disposed of', ( WidgetTester tester, ) async { - await tester.pumpWidget(GoogleMap( + await tester.pumpWidget(const GoogleMap( initialCameraPosition: CameraPosition( target: LatLng(43.3608, -5.8702), ), @@ -81,15 +81,15 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { bool disposed = false; // Stream controller to inject events for testing. - final StreamController mapEventStreamController = - StreamController.broadcast(); + final StreamController> mapEventStreamController = + StreamController>.broadcast(); @override Future init(int mapId) async {} @override - Future updateMapOptions( - Map optionsUpdate, { + Future updateMapConfiguration( + MapConfiguration update, { required int mapId, }) async {} @@ -151,7 +151,8 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { Future getVisibleRegion({ required int mapId, }) async { - return LatLngBounds(southwest: LatLng(0, 0), northeast: LatLng(0, 0)); + return LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); } @override @@ -159,7 +160,7 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { LatLng latLng, { required int mapId, }) async { - return ScreenCoordinate(x: 0, y: 0); + return const ScreenCoordinate(x: 0, y: 0); } @override @@ -167,7 +168,7 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { ScreenCoordinate screenCoordinate, { required int mapId, }) async { - return LatLng(0, 0); + return const LatLng(0, 0); } @override @@ -229,6 +230,16 @@ class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { return mapEventStreamController.stream.whereType(); } + @override + Stream onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + @override Stream onMarkerDragEnd({required int mapId}) { return mapEventStreamController.stream.whereType(); @@ -265,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/marker_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart index e295393fe15a..b5bba55671c8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart @@ -35,7 +35,7 @@ void main() { }); testWidgets('Initializing a marker', (WidgetTester tester) async { - final Marker m1 = Marker(markerId: MarkerId("marker_1")); + const Marker m1 = Marker(markerId: MarkerId('marker_1')); await tester.pumpWidget(_mapWithMarkers({m1})); final FakePlatformGoogleMap platformGoogleMap = @@ -48,9 +48,9 @@ void main() { expect(platformGoogleMap.markersToChange.isEmpty, true); }); - testWidgets("Adding a marker", (WidgetTester tester) async { - final Marker m1 = Marker(markerId: MarkerId("marker_1")); - final Marker m2 = Marker(markerId: MarkerId("marker_2")); + testWidgets('Adding a marker', (WidgetTester tester) async { + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_2')); await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m1, m2})); @@ -67,8 +67,8 @@ void main() { expect(platformGoogleMap.markersToChange.isEmpty, true); }); - testWidgets("Removing a marker", (WidgetTester tester) async { - final Marker m1 = Marker(markerId: MarkerId("marker_1")); + testWidgets('Removing a marker', (WidgetTester tester) async { + const Marker m1 = Marker(markerId: MarkerId('marker_1')); await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({})); @@ -82,9 +82,9 @@ void main() { expect(platformGoogleMap.markersToAdd.isEmpty, true); }); - testWidgets("Updating a marker", (WidgetTester tester) async { - final Marker m1 = Marker(markerId: MarkerId("marker_1")); - final Marker m2 = Marker(markerId: MarkerId("marker_1"), alpha: 0.5); + testWidgets('Updating a marker', (WidgetTester tester) async { + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_1'), alpha: 0.5); await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m2})); @@ -98,11 +98,11 @@ void main() { expect(platformGoogleMap.markersToAdd.isEmpty, true); }); - testWidgets("Updating a marker", (WidgetTester tester) async { - final Marker m1 = Marker(markerId: MarkerId("marker_1")); - final Marker m2 = Marker( - markerId: MarkerId("marker_1"), - infoWindow: const InfoWindow(snippet: 'changed'), + testWidgets('Updating a marker', (WidgetTester tester) async { + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker( + markerId: MarkerId('marker_1'), + infoWindow: InfoWindow(snippet: 'changed'), ); await tester.pumpWidget(_mapWithMarkers({m1})); @@ -117,12 +117,12 @@ void main() { expect(update.infoWindow.snippet, 'changed'); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Marker m1 = Marker(markerId: MarkerId("marker_1")); - Marker m2 = Marker(markerId: MarkerId("marker_2")); + testWidgets('Multi Update', (WidgetTester tester) async { + Marker m1 = const Marker(markerId: MarkerId('marker_1')); + Marker m2 = const Marker(markerId: MarkerId('marker_2')); final Set prev = {m1, m2}; - m1 = Marker(markerId: MarkerId("marker_1"), visible: false); - m2 = Marker(markerId: MarkerId("marker_2"), draggable: true); + m1 = const Marker(markerId: MarkerId('marker_1'), visible: false); + m2 = const Marker(markerId: MarkerId('marker_2'), draggable: true); final Set cur = {m1, m2}; await tester.pumpWidget(_mapWithMarkers(prev)); @@ -136,14 +136,14 @@ void main() { expect(platformGoogleMap.markersToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Marker m2 = Marker(markerId: MarkerId("marker_2")); - final Marker m3 = Marker(markerId: MarkerId("marker_3")); + testWidgets('Multi Update', (WidgetTester tester) async { + Marker m2 = const Marker(markerId: MarkerId('marker_2')); + const Marker m3 = Marker(markerId: MarkerId('marker_3')); final Set prev = {m2, m3}; // m1 is added, m2 is updated, m3 is removed. - final Marker m1 = Marker(markerId: MarkerId("marker_1")); - m2 = Marker(markerId: MarkerId("marker_2"), draggable: true); + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + m2 = const Marker(markerId: MarkerId('marker_2'), draggable: true); final Set cur = {m1, m2}; await tester.pumpWidget(_mapWithMarkers(prev)); @@ -161,12 +161,12 @@ void main() { expect(platformGoogleMap.markerIdsToRemove.first, equals(m3.markerId)); }); - testWidgets("Partial Update", (WidgetTester tester) async { - final Marker m1 = Marker(markerId: MarkerId("marker_1")); - final Marker m2 = Marker(markerId: MarkerId("marker_2")); - Marker m3 = Marker(markerId: MarkerId("marker_3")); + testWidgets('Partial Update', (WidgetTester tester) async { + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_2')); + Marker m3 = const Marker(markerId: MarkerId('marker_3')); final Set prev = {m1, m2, m3}; - m3 = Marker(markerId: MarkerId("marker_3"), draggable: true); + m3 = const Marker(markerId: MarkerId('marker_3'), draggable: true); final Set cur = {m1, m2, m3}; await tester.pumpWidget(_mapWithMarkers(prev)); @@ -180,12 +180,12 @@ void main() { expect(platformGoogleMap.markersToAdd.isEmpty, true); }); - testWidgets("Update non platform related attr", (WidgetTester tester) async { - Marker m1 = Marker(markerId: MarkerId("marker_1")); + testWidgets('Update non platform related attr', (WidgetTester tester) async { + Marker m1 = const Marker(markerId: MarkerId('marker_1')); final Set prev = {m1}; m1 = Marker( - markerId: MarkerId("marker_1"), - onTap: () => print("hello"), + markerId: const MarkerId('marker_1'), + onTap: () => print('hello'), onDragEnd: (LatLng latLng) => print(latLng)); final Set cur = {m1}; 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 79c63c1c5459..cb7263c02e05 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 @@ -23,9 +23,9 @@ List _rectPoints({ required double size, LatLng center = const LatLng(0, 0), }) { - final halfSize = size / 2; + final double halfSize = size / 2; - return [ + return [ LatLng(center.latitude + halfSize, center.longitude + halfSize), LatLng(center.latitude - halfSize, center.longitude + halfSize), LatLng(center.latitude - halfSize, center.longitude - halfSize), @@ -38,7 +38,7 @@ Polygon _polygonWithPointsAndHole(PolygonId polygonId) { return Polygon( polygonId: polygonId, points: _rectPoints(size: 1), - holes: [_rectPoints(size: 0.5)], + holes: >[_rectPoints(size: 0.5)], ); } @@ -58,7 +58,7 @@ void main() { }); testWidgets('Initializing a polygon', (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = @@ -71,9 +71,9 @@ void main() { expect(platformGoogleMap.polygonsToChange.isEmpty, true); }); - testWidgets("Adding a polygon", (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + testWidgets('Adding a polygon', (WidgetTester tester) async { + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p1, p2})); @@ -90,8 +90,8 @@ void main() { expect(platformGoogleMap.polygonsToChange.isEmpty, true); }); - testWidgets("Removing a polygon", (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + testWidgets('Removing a polygon', (WidgetTester tester) async { + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({})); @@ -105,10 +105,10 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Updating a polygon", (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = - Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); + testWidgets('Updating a polygon', (WidgetTester tester) async { + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = + Polygon(polygonId: PolygonId('polygon_1'), geodesic: true); await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p2})); @@ -122,10 +122,11 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Mutate a polygon", (WidgetTester tester) async { + testWidgets('Mutate a polygon', (WidgetTester tester) async { + final List _points = [const LatLng(0.0, 0.0)]; final Polygon p1 = Polygon( - polygonId: PolygonId("polygon_1"), - points: [const LatLng(0.0, 0.0)], + polygonId: const PolygonId('polygon_1'), + points: _points, ); await tester.pumpWidget(_mapWithPolygons({p1})); @@ -141,12 +142,12 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + testWidgets('Multi Update', (WidgetTester tester) async { + Polygon p1 = const Polygon(polygonId: PolygonId('polygon_1')); + Polygon p2 = const Polygon(polygonId: PolygonId('polygon_2')); final Set prev = {p1, p2}; - p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); - p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); + p1 = const Polygon(polygonId: PolygonId('polygon_1'), visible: false); + p2 = const Polygon(polygonId: PolygonId('polygon_2'), geodesic: true); final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolygons(prev)); @@ -160,14 +161,14 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); - final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); + testWidgets('Multi Update', (WidgetTester tester) async { + Polygon p2 = const Polygon(polygonId: PolygonId('polygon_2')); + const Polygon p3 = Polygon(polygonId: PolygonId('polygon_3')); final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + p2 = const Polygon(polygonId: PolygonId('polygon_2'), geodesic: true); final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolygons(prev)); @@ -185,12 +186,12 @@ void main() { expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); }); - testWidgets("Partial Update", (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); - Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); + testWidgets('Partial Update', (WidgetTester tester) async { + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); + Polygon p3 = const Polygon(polygonId: PolygonId('polygon_3')); final Set prev = {p1, p2, p3}; - p3 = Polygon(polygonId: PolygonId("polygon_3"), geodesic: true); + p3 = const Polygon(polygonId: PolygonId('polygon_3'), geodesic: true); final Set cur = {p1, p2, p3}; await tester.pumpWidget(_mapWithPolygons(prev)); @@ -204,10 +205,11 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Update non platform related attr", (WidgetTester tester) async { - Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + testWidgets('Update non platform related attr', (WidgetTester tester) async { + Polygon p1 = const Polygon(polygonId: PolygonId('polygon_1')); final Set prev = {p1}; - p1 = Polygon(polygonId: PolygonId("polygon_1"), onTap: () => print(2 + 2)); + p1 = Polygon( + polygonId: const PolygonId('polygon_1'), onTap: () => print(2 + 2)); final Set cur = {p1}; await tester.pumpWidget(_mapWithPolygons(prev)); @@ -223,7 +225,7 @@ void main() { testWidgets('Initializing a polygon with points and hole', (WidgetTester tester) async { - final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + final Polygon p1 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = @@ -236,10 +238,10 @@ void main() { expect(platformGoogleMap.polygonsToChange.isEmpty, true); }); - testWidgets("Adding a polygon with points and hole", + testWidgets('Adding a polygon with points and hole', (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = _polygonWithPointsAndHole(PolygonId("polygon_2")); + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + final Polygon p2 = _polygonWithPointsAndHole(const PolygonId('polygon_2')); await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p1, p2})); @@ -256,9 +258,9 @@ void main() { expect(platformGoogleMap.polygonsToChange.isEmpty, true); }); - testWidgets("Removing a polygon with points and hole", + testWidgets('Removing a polygon with points and hole', (WidgetTester tester) async { - final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + final Polygon p1 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({})); @@ -272,10 +274,10 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Updating a polygon by adding points and hole", + testWidgets('Updating a polygon by adding points and hole', (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + final Polygon p2 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p2})); @@ -289,12 +291,12 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Mutate a polygon with points and holes", + testWidgets('Mutate a polygon with points and holes', (WidgetTester tester) async { final Polygon p1 = Polygon( - polygonId: PolygonId("polygon_1"), + polygonId: const PolygonId('polygon_1'), points: _rectPoints(size: 1), - holes: [_rectPoints(size: 0.5)], + holes: >[_rectPoints(size: 0.5)], ); await tester.pumpWidget(_mapWithPolygons({p1})); @@ -303,7 +305,7 @@ void main() { ..addAll(_rectPoints(size: 2)); p1.holes ..clear() - ..addAll([_rectPoints(size: 1)]); + ..addAll(>[_rectPoints(size: 1)]); await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = @@ -315,19 +317,19 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Multi Update polygons with points and hole", + testWidgets('Multi Update polygons with points and hole', (WidgetTester tester) async { - Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + Polygon p1 = const Polygon(polygonId: PolygonId('polygon_1')); Polygon p2 = Polygon( - polygonId: PolygonId("polygon_2"), + polygonId: const PolygonId('polygon_2'), points: _rectPoints(size: 2), - holes: [_rectPoints(size: 1)], + holes: >[_rectPoints(size: 1)], ); final Set prev = {p1, p2}; - p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); + p1 = const Polygon(polygonId: PolygonId('polygon_1'), visible: false); p2 = p2.copyWith( pointsParam: _rectPoints(size: 5), - holesParam: [_rectPoints(size: 2)], + holesParam: >[_rectPoints(size: 2)], ); final Set cur = {p1, p2}; @@ -342,21 +344,21 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Multi Update polygons with points and hole", + testWidgets('Multi Update polygons with points and hole', (WidgetTester tester) async { Polygon p2 = Polygon( - polygonId: PolygonId("polygon_2"), + polygonId: const PolygonId('polygon_2'), points: _rectPoints(size: 2), - holes: [_rectPoints(size: 1)], + holes: >[_rectPoints(size: 1)], ); - final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); + const Polygon p3 = Polygon(polygonId: PolygonId('polygon_3')); final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. - final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + final Polygon p1 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); p2 = p2.copyWith( pointsParam: _rectPoints(size: 5), - holesParam: [_rectPoints(size: 3)], + holesParam: >[_rectPoints(size: 3)], ); final Set cur = {p1, p2}; @@ -375,19 +377,19 @@ void main() { expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); }); - testWidgets("Partial Update polygons with points and hole", + testWidgets('Partial Update polygons with points and hole', (WidgetTester tester) async { - final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); - final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + final Polygon p1 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); Polygon p3 = Polygon( - polygonId: PolygonId("polygon_3"), + polygonId: const PolygonId('polygon_3'), points: _rectPoints(size: 2), - holes: [_rectPoints(size: 1)], + holes: >[_rectPoints(size: 1)], ); final Set prev = {p1, p2, p3}; p3 = p3.copyWith( pointsParam: _rectPoints(size: 5), - holesParam: [_rectPoints(size: 3)], + holesParam: >[_rectPoints(size: 3)], ); final Set cur = {p1, p2, p3}; 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 01eb2e2ce724..9cbba3a510ca 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 @@ -35,7 +35,7 @@ void main() { }); testWidgets('Initializing a polyline', (WidgetTester tester) async { - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); await tester.pumpWidget(_mapWithPolylines({p1})); final FakePlatformGoogleMap platformGoogleMap = @@ -48,9 +48,9 @@ void main() { expect(platformGoogleMap.polylinesToChange.isEmpty, true); }); - testWidgets("Adding a polyline", (WidgetTester tester) async { - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); + testWidgets('Adding a polyline', (WidgetTester tester) async { + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = Polyline(polylineId: PolylineId('polyline_2')); await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p1, p2})); @@ -67,8 +67,8 @@ void main() { expect(platformGoogleMap.polylinesToChange.isEmpty, true); }); - testWidgets("Removing a polyline", (WidgetTester tester) async { - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); + testWidgets('Removing a polyline', (WidgetTester tester) async { + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({})); @@ -82,10 +82,10 @@ void main() { expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); - testWidgets("Updating a polyline", (WidgetTester tester) async { - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - final Polyline p2 = - Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); + testWidgets('Updating a polyline', (WidgetTester tester) async { + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = + Polyline(polylineId: PolylineId('polyline_1'), geodesic: true); await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p2})); @@ -99,10 +99,10 @@ void main() { expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); - testWidgets("Updating a polyline", (WidgetTester tester) async { - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - final Polyline p2 = - Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); + testWidgets('Updating a polyline', (WidgetTester tester) async { + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = + Polyline(polylineId: PolylineId('polyline_1'), geodesic: true); await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p2})); @@ -116,10 +116,11 @@ void main() { expect(update.geodesic, true); }); - testWidgets("Mutate a polyline", (WidgetTester tester) async { + testWidgets('Mutate a polyline', (WidgetTester tester) async { + final List _points = [const LatLng(0.0, 0.0)]; final Polyline p1 = Polyline( - polylineId: PolylineId("polyline_1"), - points: [const LatLng(0.0, 0.0)], + polylineId: const PolylineId('polyline_1'), + points: _points, ); await tester.pumpWidget(_mapWithPolylines({p1})); @@ -135,12 +136,12 @@ void main() { expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); + testWidgets('Multi Update', (WidgetTester tester) async { + Polyline p1 = const Polyline(polylineId: PolylineId('polyline_1')); + Polyline p2 = const Polyline(polylineId: PolylineId('polyline_2')); final Set prev = {p1, p2}; - p1 = Polyline(polylineId: PolylineId("polyline_1"), visible: false); - p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); + p1 = const Polyline(polylineId: PolylineId('polyline_1'), visible: false); + p2 = const Polyline(polylineId: PolylineId('polyline_2'), geodesic: true); final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolylines(prev)); @@ -154,14 +155,14 @@ void main() { expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { - Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); - final Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); + testWidgets('Multi Update', (WidgetTester tester) async { + Polyline p2 = const Polyline(polylineId: PolylineId('polyline_2')); + const Polyline p3 = Polyline(polylineId: PolylineId('polyline_3')); final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + p2 = const Polyline(polylineId: PolylineId('polyline_2'), geodesic: true); final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolylines(prev)); @@ -179,12 +180,12 @@ void main() { expect(platformGoogleMap.polylineIdsToRemove.first, equals(p3.polylineId)); }); - testWidgets("Partial Update", (WidgetTester tester) async { - final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); - Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); + testWidgets('Partial Update', (WidgetTester tester) async { + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = Polyline(polylineId: PolylineId('polyline_2')); + Polyline p3 = const Polyline(polylineId: PolylineId('polyline_3')); final Set prev = {p1, p2, p3}; - p3 = Polyline(polylineId: PolylineId("polyline_3"), geodesic: true); + p3 = const Polyline(polylineId: PolylineId('polyline_3'), geodesic: true); final Set cur = {p1, p2, p3}; await tester.pumpWidget(_mapWithPolylines(prev)); @@ -198,11 +199,12 @@ void main() { expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); - testWidgets("Update non platform related attr", (WidgetTester tester) async { - Polyline p1 = Polyline(polylineId: PolylineId("polyline_1"), onTap: null); + testWidgets('Update non platform related attr', (WidgetTester tester) async { + Polyline p1 = + const Polyline(polylineId: PolylineId('polyline_1'), onTap: null); final Set prev = {p1}; p1 = Polyline( - polylineId: PolylineId("polyline_1"), onTap: () => print(2 + 2)); + polylineId: const PolylineId('polyline_1'), onTap: () => print(2 + 2)); final Set cur = {p1}; await tester.pumpWidget(_mapWithPolylines(prev)); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart index 35732da29726..b4586f743296 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart @@ -33,8 +33,8 @@ void main() { }); testWidgets('Initializing a tile overlay', (WidgetTester tester) async { - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); await tester.pumpWidget(_mapWithTileOverlays({t1})); final FakePlatformGoogleMap platformGoogleMap = @@ -48,11 +48,11 @@ void main() { expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); }); - testWidgets("Adding a tile overlay", (WidgetTester tester) async { - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); - final TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + testWidgets('Adding a tile overlay', (WidgetTester tester) async { + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); + const TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t1, t2})); @@ -69,9 +69,9 @@ void main() { expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); }); - testWidgets("Removing a tile overlay", (WidgetTester tester) async { - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + testWidgets('Removing a tile overlay', (WidgetTester tester) async { + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({})); @@ -86,11 +86,11 @@ void main() { expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); }); - testWidgets("Updating a tile overlay", (WidgetTester tester) async { - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); - final TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + testWidgets('Updating a tile overlay', (WidgetTester tester) async { + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); + const TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1'), zIndex: 10); await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t2})); @@ -104,11 +104,11 @@ void main() { expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); }); - testWidgets("Updating a tile overlay", (WidgetTester tester) async { - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); - final TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + testWidgets('Updating a tile overlay', (WidgetTester tester) async { + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); + const TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1'), zIndex: 10); await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t2})); @@ -122,16 +122,16 @@ void main() { expect(update.zIndex, 10); }); - testWidgets("Multi Update", (WidgetTester tester) async { + testWidgets('Multi Update', (WidgetTester tester) async { TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); final Set prev = {t1, t2}; - t1 = TileOverlay( - tileOverlayId: TileOverlayId("tile_overlay_1"), visible: false); - t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + t1 = const TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), visible: false); + t2 = const TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_2'), zIndex: 10); final Set cur = {t1, t2}; await tester.pumpWidget(_mapWithTileOverlays(prev)); @@ -145,18 +145,18 @@ void main() { expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); }); - testWidgets("Multi Update", (WidgetTester tester) async { + testWidgets('Multi Update', (WidgetTester tester) async { TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); - final TileOverlay t3 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); + const TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_3')); final Set prev = {t2, t3}; // t1 is added, t2 is updated, t3 is removed. - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); - t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); + t2 = const TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_2'), zIndex: 10); final Set cur = {t1, t2}; await tester.pumpWidget(_mapWithTileOverlays(prev)); @@ -175,16 +175,16 @@ void main() { equals(t3.tileOverlayId)); }); - testWidgets("Partial Update", (WidgetTester tester) async { - final TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); - final TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + testWidgets('Partial Update', (WidgetTester tester) async { + const TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); + const TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); TileOverlay t3 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_3')); final Set prev = {t1, t2, t3}; - t3 = - TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"), zIndex: 10); + t3 = const TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_3'), zIndex: 10); final Set cur = {t1, t2, t3}; await tester.pumpWidget(_mapWithTileOverlays(prev)); 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 5d361d8e0c7c..c1a7c95abfdb 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,39 @@ +## 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`. + +## 2.1.4 + +* Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. + +## 2.1.3 + +* `LatLng` constructor maintains longitude precision when given within + acceptable range + +## 2.1.2 + +* Add additional marker drag events + ## 2.1.1 * Method `buildViewWithTextDirection` has been added to the platform interface. 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..b83eaf4fdfc7 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,8 @@ // 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/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 be426483193d..8759126d4b67 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 @@ -3,7 +3,6 @@ // 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'; /// 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 @@ -102,6 +101,26 @@ class InfoWindowTapEvent extends MapEvent { InfoWindowTapEvent(int mapId, MarkerId markerId) : super(mapId, markerId); } +/// An event fired when a [Marker] is starting to be dragged to a new [LatLng]. +class MarkerDragStartEvent extends _PositionedMapEvent { + /// Build a MarkerDragStart Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was picked up from. + /// The `value` of this event is a [MarkerId] object that represents the Marker. + MarkerDragStartEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + +/// An event fired when a [Marker] is being dragged to a new [LatLng]. +class MarkerDragEvent extends _PositionedMapEvent { + /// Build a MarkerDrag Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was dragged to. + /// The `value` of this event is a [MarkerId] object that represents the Marker. + MarkerDragEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + /// An event fired when a [Marker] is dragged to a new [LatLng]. class MarkerDragEndEvent extends _PositionedMapEvent { /// Build a MarkerDragEnd Event triggered from the map represented by `mapId`. 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 2b9c71ee85bd..a34ee48ac79a 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'; @@ -14,7 +16,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:stream_transform/stream_transform.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}) { @@ -124,6 +129,16 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { 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(); @@ -171,38 +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'] 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']), + 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': @@ -220,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: @@ -307,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 = @@ -357,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); } } @@ -395,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 @@ -456,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) { @@ -490,8 +513,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ) { return AndroidViewSurface( controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, + gestureRecognizers: widgetConfiguration.gestureRecognizers, hitTestBehavior: PlatformViewHitTestBehavior.opaque, ); }, @@ -500,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), @@ -520,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(), ); @@ -529,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(), ); @@ -539,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 2bb0ab2588f9..b6b95018d0c4 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,16 +3,17 @@ // 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:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/map_configuration_serialization.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The interface that platform-specific implementations of `google_maps_flutter` must extend. @@ -39,7 +40,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [GoogleMapsFlutterPlatform] when they register themselves. static set instance(GoogleMapsFlutterPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } @@ -50,7 +51,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 +65,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 @@ -303,6 +319,16 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('onInfoWindowTap() has not been implemented.'); } + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDragStart({required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDrag({required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + /// A [Marker] has been dragged to a different [LatLng] position. Stream onMarkerDragEnd({required int mapId}) { throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); @@ -338,7 +364,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, { @@ -350,14 +377,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. @@ -371,12 +399,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( @@ -392,4 +420,27 @@ 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), + ); + } } 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..0ccc3e624abe 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, ], @@ -117,51 +161,6 @@ class BitmapDescriptor { return BitmapDescriptor._([_fromBytes, byteData]); } - /// The inverse of .toJson. - // This is needed in Web to re-hydrate BitmapDescriptors that have been - // transformed to JSON for transport. - // TODO(https://github.com/flutter/flutter/issues/70330): Clean this up. - BitmapDescriptor.fromJson(Object json) : _json = json { - assert(_json is List); - final jsonList = json as List; - assert(_validTypes.contains(jsonList[0])); - switch (jsonList[0]) { - case _defaultMarker: - assert(jsonList.length <= 2); - if (jsonList.length == 2) { - assert(jsonList[1] is num); - assert(0 <= jsonList[1] && jsonList[1] < 360); - } - break; - case _fromBytes: - assert(jsonList.length == 2); - assert(jsonList[1] != null && jsonList[1] is List); - assert((jsonList[1] as List).isNotEmpty); - break; - case _fromAsset: - assert(jsonList.length <= 3); - assert(jsonList[1] != null && jsonList[1] is String); - assert((jsonList[1] as String).isNotEmpty); - if (jsonList.length == 3) { - assert(jsonList[2] != null && jsonList[2] is String); - assert((jsonList[2] as String).isNotEmpty); - } - break; - case _fromAssetImage: - assert(jsonList.length <= 4); - assert(jsonList[1] != null && jsonList[1] is String); - assert((jsonList[1] as String).isNotEmpty); - assert(jsonList[2] != null && jsonList[2] is double); - if (jsonList.length == 4) { - assert(jsonList[3] != null && jsonList[3] is List); - assert((jsonList[3] as List).length == 2); - } - break; - default: - break; - } - } - final Object _json; /// Convert the object to a Json format. 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/cap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart index f5f43209d828..5bef7baf0bf4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart @@ -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:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable; import 'types.dart'; 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 1845195b31c6..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/foundation.dart' show immutable; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable; 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/joint_type.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart index 64e7a3d8cbdc..b986025b27a6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.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:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable; /// Joint types for [Polyline]. @immutable 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 42c66e036fd7..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:meta/meta.dart'; +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]. @@ -14,13 +14,16 @@ class LatLng { /// The latitude is clamped to the inclusive interval from -90.0 to +90.0. /// /// The longitude is normalized to the half-open interval from -180.0 - /// (inclusive) to +180.0 (exclusive) + /// (inclusive) to +180.0 (exclusive). const LatLng(double latitude, double longitude) : assert(latitude != null), assert(longitude != null), latitude = - (latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude)), - longitude = (longitude + 180.0) % 360.0 - 180.0; + 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 + : (longitude + 180.0) % 360.0 - 180.0; /// The latitude in degrees between -90.0 and 90.0, both inclusive. final double latitude; @@ -39,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. @@ -63,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. /// @@ -109,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])!, @@ -118,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 77d958be01e2..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 @@ -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:flutter/foundation.dart' show objectRuntimeType; -import 'package:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable, objectRuntimeType; /// Uniquely identifies object an among [GoogleMap] collections of a specific /// type. @@ -21,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..0051afcefbab 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 @@ -45,7 +45,7 @@ class MapsObjectUpdates { // 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; } @@ -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 0d1b780c24d2..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,10 +2,10 @@ // 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 ValueChanged, VoidCallback; -import 'package:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' + show immutable, ValueChanged, VoidCallback; import 'types.dart'; @@ -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. @@ -147,6 +152,8 @@ class Marker implements MapsObject { this.visible = true, this.zIndex = 0.0, this.onTap, + this.onDrag, + this.onDragStart, this.onDragEnd, }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); @@ -207,9 +214,15 @@ class Marker implements MapsObject { /// Callbacks to receive tap events for markers placed on this map. final VoidCallback? onTap; + /// Signature reporting the new [LatLng] at the start of a drag event. + final ValueChanged? onDragStart; + /// Signature reporting the new [LatLng] at the end of a drag event. final ValueChanged? onDragEnd; + /// Signature reporting the new [LatLng] during the drag event. + final ValueChanged? onDrag; + /// Creates a new [Marker] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Marker copyWith({ @@ -225,6 +238,8 @@ class Marker implements MapsObject { bool? visibleParam, double? zIndexParam, VoidCallback? onTapParam, + ValueChanged? onDragStartParam, + ValueChanged? onDragParam, ValueChanged? onDragEndParam, }) { return Marker( @@ -241,14 +256,18 @@ class Marker implements MapsObject { visible: visibleParam ?? visible, zIndex: zIndexParam ?? zIndex, onTap: onTapParam ?? onTap, + onDragStart: onDragStartParam ?? onDragStart, + onDrag: onDragParam ?? onDrag, onDragEnd: onDragEndParam ?? onDragEnd, ); } /// 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 = {}; @@ -275,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 @@ -300,6 +323,7 @@ class Marker implements MapsObject { return 'Marker{markerId: $markerId, alpha: $alpha, anchor: $anchor, ' 'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, ' 'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, ' - 'visible: $visible, zIndex: $zIndex, onTap: $onTap}'; + 'visible: $visible, zIndex: $zIndex, onTap: $onTap, onDragStart: $onDragStart, ' + 'onDrag: $onDrag, onDragEnd: $onDragEnd}'; } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart index 89f29d25e4cc..033210b8c5ae 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart @@ -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:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable; /// Item used in the stroke pattern for a Polyline. @immutable 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 569bd4c1f553..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 @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart' show listEquals, VoidCallback; +import 'package:flutter/foundation.dart' + show immutable, listEquals, VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable; import 'types.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 c324aeb5f492..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 @@ -2,9 +2,9 @@ // 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 listEquals, VoidCallback; +import 'package:flutter/foundation.dart' + show immutable, listEquals, VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable; import 'types.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 8c9c083913ce..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:meta/meta.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.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart index d602b127f06c..d73701511059 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:typed_data'; -import 'package:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable; /// Contains information about a Tile that is returned by a [TileProvider]. @immutable 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 8cdd2c4699e1..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,10 +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 'package:meta/meta.dart' show immutable; +import 'package:flutter/foundation.dart' show immutable; import 'types.dart'; @@ -44,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, @@ -109,9 +106,11 @@ class TileOverlay implements MapsObject { ); } + @override TileOverlay clone() => copyWith(); /// Converts this object to JSON. + @override Object toJson() { final Map json = {}; @@ -147,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 1dc73f442d2e..2b01e6244210 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 @@ -1,24 +1,24 @@ name: google_maps_flutter_platform_interface description: A common platform interface for the google_maps_flutter plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_platform_interface 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.1 +version: 2.2.0 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: collection: ^1.15.0 flutter: sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 stream_transform: ^2.0.0 dev_dependencies: + async: ^2.5.0 flutter_test: sdk: flutter mockito: ^5.0.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 19e81c960839..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,10 +2,9 @@ // 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/method_channel/method_channel_google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; void main() { @@ -33,6 +32,15 @@ void main() { }); } + 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.io/google_maps_$mapId', + byteData, (ByteData? data) {}); + } + // Calls each method that uses invokeMethod with a return type other than // void to ensure that the casting/nullability handling succeeds. // @@ -56,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. @@ -68,5 +76,47 @@ void main() { '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 MethodChannelGoogleMapsFlutter maps = + MethodChannelGoogleMapsFlutter(); + 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/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 de4edf375696..d185aabe1a5c 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,20 +6,21 @@ 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(); + // Store the initial instance before any tests change it. + final GoogleMapsFlutterPlatform initialInstance = + GoogleMapsFlutterPlatform.instance; + group('$GoogleMapsFlutterPlatform', () { test('$MethodChannelGoogleMapsFlutter() is the default instance', () { - expect(GoogleMapsFlutterPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Cannot be implemented with `implements`', () { @@ -48,13 +49,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/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart index 6d02b2c630df..7fbaf4998355 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 @@ -13,13 +13,14 @@ 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 @@ -28,81 +29,85 @@ void main() { 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 +115,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 +151,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 new file mode 100644 index 000000000000..9da3e543ea58 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.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_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LanLng constructor', () { + test('Maintains longitude precision if within acceptable range', () async { + const double lat = -34.509981; + const double lng = 150.792384; + + 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 double lat = -34.509981; + const double lng = -270.0; + + 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 double lat = -34.509981; + const double lng = 270.0; + + 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 double lat = -34.509981; + const double lng = -180.0; + + 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 double lat = -34.509981; + const double lng = 180.0; + + 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 new file mode 100644 index 000000000000..db7afcbb0398 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.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 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$Marker', () { + test('constructor defaults', () { + const Marker marker = Marker(markerId: MarkerId('ABC123')); + + expect(marker.alpha, equals(1.0)); + expect(marker.anchor, equals(const Offset(0.5, 1.0))); + expect(marker.consumeTapEvents, equals(false)); + expect(marker.draggable, equals(false)); + expect(marker.flat, equals(false)); + expect(marker.icon, equals(BitmapDescriptor.defaultMarker)); + expect(marker.infoWindow, equals(InfoWindow.noText)); + expect(marker.position, equals(const LatLng(0.0, 0.0))); + expect(marker.rotation, equals(0.0)); + expect(marker.visible, equals(true)); + expect(marker.zIndex, equals(0.0)); + expect(marker.onTap, equals(null)); + expect(marker.onDrag, equals(null)); + expect(marker.onDragStart, equals(null)); + expect(marker.onDragEnd, equals(null)); + }); + test('constructor alpha is >= 0.0 and <= 1.0', () { + 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)); + expect(() => initWithAlpha(1.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(100), throwsAssertionError); + }); + + test('toJson', () { + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final Marker marker = Marker( + markerId: const MarkerId('ABC123'), + alpha: 0.12345, + anchor: const Offset(100, 100), + consumeTapEvents: true, + draggable: true, + flat: true, + icon: testDescriptor, + infoWindow: const InfoWindow( + title: 'Test title', + snippet: 'Test snippet', + anchor: Offset(100, 200), + ), + position: const LatLng(50, 50), + rotation: 100, + visible: false, + zIndex: 100, + onTap: () {}, + onDragStart: (LatLng latLng) {}, + onDrag: (LatLng latLng) {}, + onDragEnd: (LatLng latLng) {}, + ); + + final Map json = marker.toJson() as Map; + + expect(json, { + 'markerId': 'ABC123', + 'alpha': 0.12345, + 'anchor': [100, 100], + 'consumeTapEvents': true, + 'draggable': true, + 'flat': true, + 'icon': testDescriptor.toJson(), + 'infoWindow': { + 'title': 'Test title', + 'snippet': 'Test snippet', + 'anchor': [100.0, 200.0], + }, + 'position': [50, 50], + 'rotation': 100.0, + 'visible': false, + 'zIndex': 100.0, + }); + }); + test('clone', () { + const Marker marker = Marker(markerId: MarkerId('ABC123')); + final Marker clone = marker.clone(); + + expect(identical(clone, marker), isFalse); + expect(clone, equals(marker)); + }); + test('copyWith', () { + const Marker marker = Marker(markerId: MarkerId('ABC123')); + + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + 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; + const InfoWindow testInfoWindowParam = InfoWindow(title: 'Test'); + const LatLng testPositionParam = LatLng(100, 100); + const double testRotationParam = 100; + final bool testVisibleParam = !marker.visible; + const double testZIndexParam = 100; + final List log = []; + + final Marker copy = marker.copyWith( + alphaParam: testAlphaParam, + anchorParam: testAnchorParam, + consumeTapEventsParam: testConsumeTapEventsParam, + draggableParam: testDraggableParam, + flatParam: testFlatParam, + iconParam: testIconParam, + infoWindowParam: testInfoWindowParam, + positionParam: testPositionParam, + rotationParam: testRotationParam, + visibleParam: testVisibleParam, + zIndexParam: testZIndexParam, + onTapParam: () { + log.add('onTapParam'); + }, + onDragStartParam: (LatLng latLng) { + log.add('onDragStartParam'); + }, + onDragParam: (LatLng latLng) { + log.add('onDragParam'); + }, + onDragEndParam: (LatLng latLng) { + log.add('onDragEndParam'); + }, + ); + + expect(copy.alpha, equals(testAlphaParam)); + expect(copy.anchor, equals(testAnchorParam)); + expect(copy.consumeTapEvents, equals(testConsumeTapEventsParam)); + expect(copy.draggable, equals(testDraggableParam)); + expect(copy.flat, equals(testFlatParam)); + expect(copy.icon, equals(testIconParam)); + expect(copy.infoWindow, equals(testInfoWindowParam)); + expect(copy.position, equals(testPositionParam)); + expect(copy.rotation, equals(testRotationParam)); + expect(copy.visible, equals(testVisibleParam)); + expect(copy.zIndex, equals(testZIndexParam)); + + copy.onTap!(); + expect(log, contains('onTapParam')); + + copy.onDragStart!(const LatLng(0, 1)); + expect(log, contains('onDragStartParam')); + + copy.onDrag!(const LatLng(0, 1)); + expect(log, contains('onDragParam')); + + 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..1a9a9d480f1a 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); } } @@ -67,7 +65,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 +73,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,14 +81,14 @@ 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, @@ -107,7 +105,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 +128,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 4d7ecf74e098..d86642934b77 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,38 @@ +## 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`. + +## 0.3.2 + +* Add `onDragStart` and `onDrag` to `Marker` + ## 0.3.1 * Fix the `getScreenCoordinate(LatLng)` method. [#80710](https://github.com/flutter/flutter/issues/80710) 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/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..dd2520d418f6 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,9 +18,9 @@ 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: [ +@GenerateMocks([], customMocks: >[ MockSpec(returnNullOnMissingStub: true), MockSpec(returnNullOnMissingStub: true), MockSpec(returnNullOnMissingStub: true), @@ -32,34 +32,30 @@ 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({ 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', () { @@ -70,13 +66,13 @@ void main() { 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); }); }); }); @@ -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,7 +260,7 @@ 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.debugSetOverrides( @@ -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; }); @@ -414,7 +447,7 @@ void main() { testWidgets('sets initial position when passed', (WidgetTester tester) async { controller = _createController( - initialCameraPosition: CameraPosition( + initialCameraPosition: const CameraPosition( target: LatLng(43.308, -5.6910), zoom: 12, bearing: 0, @@ -422,7 +455,8 @@ void main() { ), ); - controller.debugSetOverrides(createMap: (_, options) { + controller.debugSetOverrides( + createMap: (_, gmaps.MapOptions options) { capturedOptions = options; return map; }); @@ -444,9 +478,10 @@ void main() { 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); @@ -472,9 +507,9 @@ void main() { 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 +517,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 +533,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 +551,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,10 +567,7 @@ 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 }); }); @@ -542,116 +578,122 @@ void main() { }); 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 af8ed5420a0c..9565935bd8ed 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,8 +1,4 @@ -// Copyright 2013 The Flutter 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.15 from annotations +// Mocks generated by Mockito 5.2.0 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. // Do not manually edit this file. @@ -12,6 +8,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf 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 @@ -19,6 +16,7 @@ import 'package:mockito/mockito.dart' as _i1; // 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 _FakeGMap_0 extends _i1.Fake implements _i2.GMap {} @@ -61,8 +59,6 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } /// A class which mocks [PolygonsController]. @@ -105,8 +101,6 @@ class MockPolygonsController extends _i1.Mock void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } /// A class which mocks [PolylinesController]. @@ -149,8 +143,6 @@ class MockPolylinesController extends _i1.Mock void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } /// A class which mocks [MarkersController]. @@ -204,6 +196,4 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { void bindToMap(int? mapId, _i2.GMap? googleMap) => super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } 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 758294f5bb91..e66a3f47c78c 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,19 +5,18 @@ 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: [ +@GenerateMocks([], customMocks: >[ MockSpec(returnNullOnMissingStub: true), ]) @@ -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 { + 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,91 +398,144 @@ 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); }); 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); }); 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); }); // 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); }); testWidgets('onInfoWindowTap', (WidgetTester tester) async { - final event = InfoWindowTapEvent(mapId, MarkerId('test-123')); + final InfoWindowTapEvent event = InfoWindowTapEvent( + mapId, + const MarkerId('test-123'), + ); + + final Stream stream = + plugin.onInfoWindowTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onMarkerDragStart', (WidgetTester tester) async { + final MarkerDragStartEvent event = MarkerDragStartEvent( + mapId, + const LatLng(43.3677, -5.8372), + const MarkerId('test-123'), + ); - final stream = plugin.onInfoWindowTap(mapId: mapId); + final Stream stream = + plugin.onMarkerDragStart(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onMarkerDrag', (WidgetTester tester) async { + final MarkerDragEvent event = MarkerDragEvent( + mapId, + const LatLng(43.3677, -5.8372), + const MarkerId('test-123'), + ); + + final Stream stream = + plugin.onMarkerDrag(mapId: mapId); 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); }); // 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); }); 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); }); 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); }); // 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); }); 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); }); @@ -456,6 +543,6 @@ void main() { }); } -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 01908ce777e7..744552f45d4d 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,18 +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. - -// Mocks generated by Mockito 5.0.15 from annotations +// Mocks generated by Mockito 5.2.0 from annotations // in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. // Do not manually edit this file. 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 @@ -20,6 +18,7 @@ import 'package:mockito/mockito.dart' as _i1; // 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 _FakeStreamController_0 extends _i1.Fake implements _i2.StreamController {} @@ -37,15 +36,15 @@ class _FakeLatLng_3 extends _i1.Fake implements _i3.LatLng {} class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapController { @override - _i2.StreamController<_i3.MapEvent> get stream => + _i2.StreamController<_i3.MapEvent> get stream => (super.noSuchMethod(Invocation.getter(#stream), - returnValue: _FakeStreamController_0<_i3.MapEvent>()) - as _i2.StreamController<_i3.MapEvent>); + returnValue: _FakeStreamController_0<_i3.MapEvent>()) + as _i2.StreamController<_i3.MapEvent>); @override - _i2.Stream<_i3.MapEvent> get events => + _i2.Stream<_i3.MapEvent> get events => (super.noSuchMethod(Invocation.getter(#events), - returnValue: Stream<_i3.MapEvent>.empty()) - as _i2.Stream<_i3.MapEvent>); + returnValue: Stream<_i3.MapEvent>.empty()) + as _i2.Stream<_i3.MapEvent>); @override bool get isInitialized => (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) @@ -70,8 +69,12 @@ class MockGoogleMapController extends _i1.Mock void init() => super.noSuchMethod(Invocation.method(#init, []), returnValueForMissingStub: null); @override - void updateRawOptions(Map? optionsUpdate) => - super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]), + void updateMapConfiguration(_i3.MapConfiguration? update) => + super.noSuchMethod(Invocation.method(#updateMapConfiguration, [update]), + returnValueForMissingStub: null); + @override + void updateStyles(List<_i5.MapTypeStyle>? styles) => + super.noSuchMethod(Invocation.method(#updateStyles, [styles]), returnValueForMissingStub: null); @override _i2.Future<_i3.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( @@ -129,6 +132,4 @@ class MockGoogleMapController extends _i1.Mock @override void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } 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 2bfa27b73a77..e07ade03bba3 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() { @@ -27,12 +27,20 @@ void main() { _methodCalledCompleter.complete(true); } + void onDragStart(gmaps.LatLng _) { + _methodCalledCompleter.complete(true); + } + + void onDrag(gmaps.LatLng _) { + _methodCalledCompleter.complete(true); + } + void onDragEnd(gmaps.LatLng _) { _methodCalledCompleter.complete(true); } setUp(() { - _methodCalledCompleter = Completer(); + _methodCalledCompleter = Completer(); methodCalled = _methodCalledCompleter.future; }); @@ -47,25 +55,52 @@ 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); }); + testWidgets('onDragStart gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDragStart: onDragStart); + + // Trigger a drag end event... + gmaps.Event.trigger(marker, 'dragstart', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDrag gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDrag: onDrag); + + // Trigger a drag end event... + gmaps.Event.trigger( + marker, + 'drag', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)], + ); + + expect(await methodCalled, isTrue); + }); + testWidgets('onDragEnd gets called', (WidgetTester tester) async { 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); @@ -76,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(); @@ -84,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(); @@ -97,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(); @@ -113,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); }); @@ -127,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..90195ec6397b 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 @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; import 'dart:js_util' show getProperty; +import 'dart:typed_data'; 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,91 +78,93 @@ 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); + expect(controller.markers[const MarkerId('1')]?.marker?.icon, isNotNull); - final blobUrl = getProperty( - controller.markers[MarkerId('1')]!.marker!.icon!, + final String blobUrl = getProperty( + controller.markers[const MarkerId('1')]!.marker!.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: @@ -169,8 +174,8 @@ void main() { // 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 +187,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 +212,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..14e4156b87ec 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,7 +208,7 @@ 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), 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..d1426760ceae 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() { @@ -27,7 +27,7 @@ void main() { } setUp(() { - _methodCalledCompleter = Completer(); + _methodCalledCompleter = Completer(); methodCalled = _methodCalledCompleter.future; }); @@ -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 249b893d198c..fb6359fe5b8f 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,24 @@ 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.8.0" dependencies: - google_maps_flutter_web: - path: ../ flutter: sdk: flutter + 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.0.0 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..250bb5468fa7 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,7 +224,7 @@ 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. } @@ -221,7 +235,7 @@ gmaps.MarkerOptions _markerOptionsFromMarker( Marker marker, gmaps.Marker? currentMarker, ) { - final iconConfig = marker.icon.toJson() as List; + final List iconConfig = marker.icon.toJson() as List; gmaps.Icon? icon; if (iconConfig != null) { @@ -231,19 +245,24 @@ gmaps.MarkerOptions _markerOptionsFromMarker( // 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 List rawIconSize = iconConfig[3]! as List; + final gmaps.Size size = gmaps.Size( + rawIconSize[0] as num?, + rawIconSize[1] as num?, + ); 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); } } @@ -253,18 +272,18 @@ gmaps.MarkerOptions _markerOptionsFromMarker( 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. + // 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 +298,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 +329,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 +362,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 +373,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 +393,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 +454,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 +463,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 +474,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 d03dec93ce3f..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. @@ -240,6 +239,16 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { 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(); @@ -278,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 5b0169b565e5..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,31 +6,41 @@ 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, gmaps.InfoWindow? infoWindow, bool consumeTapEvents = false, + LatLngCallback? onDragStart, + LatLngCallback? onDrag, LatLngCallback? onDragEnd, ui.VoidCallback? onTap, }) : _marker = marker, _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((gmaps.MapMouseEvent event) { + if (marker != null) { + marker.position = event.latLng; + } + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDrag != null) { + marker.onDrag.listen((gmaps.MapMouseEvent event) { + if (marker != null) { + marker.position = event.latLng; + } + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }); + } if (onDragEnd != null) { - marker.onDragend.listen((event) { + marker.onDragend.listen((gmaps.MapMouseEvent event) { if (marker != null) { marker.position = event.latLng; } @@ -39,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 b650b9bcf1c8..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,34 +34,43 @@ 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) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, onDragEnd: (gmaps.LatLng latLng) { _onMarkerDragEnd(marker.markerId, latLng); }, @@ -75,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?, @@ -107,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(); } @@ -115,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(); } @@ -123,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; } @@ -140,6 +151,22 @@ class MarkersController extends GeometryController { _streamController.add(InfoWindowTapEvent(mapId, markerId)); } + void _onMarkerDragStart(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragStartEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } + + void _onMarkerDrag(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } + void _onMarkerDragEnd(MarkerId markerId, gmaps.LatLng latLng) { _streamController.add(MarkerDragEndEvent( mapId, @@ -150,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..84c66264db7b 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,8 +2,8 @@ // 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 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; /// A void function that handles a [gmaps.LatLng] as a parameter. /// 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 8a23916b0e98..9670e0f28e2e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -1,12 +1,12 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter -repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web +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.1 +version: 0.4.0+1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -21,13 +21,15 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - google_maps_flutter_platform_interface: ^2.0.1 - google_maps: ^5.2.0 - meta: ^1.3.0 + google_maps: ^6.1.0 + google_maps_flutter_platform_interface: ^2.2.0 sanitize_html: ^2.0.0 stream_transform: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/web/index.html diff --git a/packages/google_sign_in/analysis_options.yaml b/packages/google_sign_in/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/google_sign_in/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 1f0be2e237b2..7e433a71368f 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,58 @@ +## 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. + +## 5.2.3 + +* Bumps the Android dependency on `com.google.android.gms:play-services-auth` and therefore removes the need for `jetifier`. + +## 5.2.2 + +* Updates Android compileSdkVersion to 31. +* Removes dependency on `meta`. + +## 5.2.1 + + Change the placeholder of the GoogleUserCircleAvatar to a transparent image. + +## 5.2.0 + +* Add `GoogleSignInAccount.serverAuthCode`. Mark `GoogleSignInAuthentication.serverAuthCode` as deprecated. + +## 5.1.1 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + ## 5.1.0 * Add reAuthenticate option to signInSilently to allow re-authentication to be requested diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 6ed21c0fedd2..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 @@ -27,6 +31,8 @@ Otherwise, you may encounter `APIException` errors. ### iOS integration +This plugin requires iOS 9.0 or higher. + 1. [First register your application](https://firebase.google.com/docs/ios/setup). 2. Make sure the file you download in step 1 is named `GoogleService-Info.plist`. @@ -59,6 +65,22 @@ Otherwise, you may encounter `APIException` errors. ``` +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 @@ -120,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/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle deleted file mode 100644 index ea98b315f147..000000000000 --- a/packages/google_sign_in/google_sign_in/android/build.gradle +++ /dev/null @@ -1,55 +0,0 @@ -group 'io.flutter.plugins.googlesignin' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - implementation 'com.google.android.gms:play-services-auth:16.0.1' - implementation 'com.google.guava:guava:20.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.9.0' -} 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/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java deleted file mode 100644 index 3b6ad960f548..000000000000 --- a/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ /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. - -package io.flutter.plugins.googlesignin; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.common.api.Scope; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; - -public class GoogleSignInTest { - @Mock Context mockContext; - @Mock Activity mockActivity; - @Mock PluginRegistry.Registrar mockRegistrar; - @Mock BinaryMessenger mockMessenger; - @Spy MethodChannel.Result result; - @Mock GoogleSignInWrapper mockGoogleSignIn; - @Mock GoogleSignInAccount account; - private GoogleSignInPlugin plugin; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mockRegistrar.messenger()).thenReturn(mockMessenger); - when(mockRegistrar.context()).thenReturn(mockContext); - when(mockRegistrar.activity()).thenReturn(mockActivity); - plugin = new GoogleSignInPlugin(); - plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); - plugin.setUpRegistrar(mockRegistrar); - } - - @Test - public void requestScopes_ResultErrorIfAccountIsNull() { - MethodCall methodCall = new MethodCall("requestScopes", null); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - plugin.onMethodCall(methodCall, result); - verify(result).error("sign_in_required", "No account to grant scopes.", null); - } - - @Test - public void requestScopes_ResultTrueIfAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); - - plugin.onMethodCall(methodCall, result); - verify(result).success(true); - } - - @Test - public void requestScopes_RequestsPermissionIfNotGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - - verify(mockGoogleSignIn) - .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); - } - - @Test - public void requestScopes_ReturnsFalseIfPermissionDenied() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - PluginRegistry.ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, - Activity.RESULT_CANCELED, - new Intent()); - - verify(result).success(false); - } - - @Test - public void requestScopes_ReturnsTrueIfPermissionGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - PluginRegistry.ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result).success(true); - } - - @Test - public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - PluginRegistry.ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result, times(2)).success(true); - } - - @Test - public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - PluginRegistry.ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); - } - - @Test(expected = IllegalStateException.class) - public void signInThrowsWithoutActivity() { - final GoogleSignInPlugin plugin = new GoogleSignInPlugin(); - plugin.initInstance( - mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class)); - - plugin.onMethodCall(new MethodCall("signIn", null), null); - } -} 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.iml b/packages/google_sign_in/google_sign_in/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle index 5d574a2c6a51..8ac99fe56f3a 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle +++ b/packages/google_sign_in/google_sign_in/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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' diff --git a/packages/google_sign_in/google_sign_in/example/android/gradle.properties b/packages/google_sign_in/google_sign_in/example/android/gradle.properties index 38c8d4544ff1..d12b9a8297e5 100644 --- a/packages/google_sign_in/google_sign_in/example/android/gradle.properties +++ b/packages/google_sign_in/google_sign_in/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.enableR8=true android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/google_sign_in/google_sign_in/example/example.iml b/packages/google_sign_in/google_sign_in/example/example.iml deleted file mode 100644 index c4447024fe3c..000000000000 --- a/packages/google_sign_in/google_sign_in/example/example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml b/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/google_sign_in/google_sign_in/example/google_sign_in_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file 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 7a1522346e37..be3cc89674a3 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 @@ -4,15 +4,15 @@ // @dart = 2.9 -import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Can initialize the plugin', (WidgetTester tester) async { - GoogleSignIn signIn = GoogleSignIn(); + final GoogleSignIn signIn = GoogleSignIn(); expect(signIn, isNotNull); }); } 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/Runner/main.m b/packages/google_sign_in/google_sign_in/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner/main.m +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +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/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 c677d4e75bc3..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 @@ -7,9 +7,9 @@ import 'dart:async'; import 'dart:convert' show json; -import "package:http/http.dart" as http; import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:http/http.dart' as http; GoogleSignIn _googleSignIn = GoogleSignIn( // Optional clientId @@ -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(); } @@ -54,7 +56,7 @@ class SignInDemoState extends State { Future _handleGetContact(GoogleSignInAccount user) async { setState(() { - _contactText = "Loading contact info..."; + _contactText = 'Loading contact info...'; }); final http.Response response = await http.get( Uri.parse('https://people.googleapis.com/v1/people/me/connections' @@ -63,36 +65,37 @@ class SignInDemoState extends State { ); if (response.statusCode != 200) { setState(() { - _contactText = "People API gave a ${response.statusCode} " - "response. Check logs for details."; + _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); + final Map data = + json.decode(response.body) as Map; final String? namedContact = _pickFirstNamedContact(data); setState(() { if (namedContact != null) { - _contactText = "I see you know $namedContact!"; + _contactText = 'I see you know $namedContact!'; } else { - _contactText = "No contacts to display."; + _contactText = 'No contacts to display.'; } }); } String? _pickFirstNamedContact(Map data) { - final List? connections = data['connections']; + final List? connections = data['connections'] as List?; final Map? contact = connections?.firstWhere( (dynamic contact) => contact['names'] != null, orElse: () => null, - ); + ) as Map?; if (contact != null) { final Map? name = contact['names'].firstWhere( (dynamic name) => name['displayName'] != null, orElse: () => null, - ); + ) as Map?; if (name != null) { - return name['displayName']; + return name['displayName'] as String?; } } return null; @@ -109,7 +112,7 @@ class SignInDemoState extends State { Future _handleSignOut() => _googleSignIn.disconnect(); Widget _buildBody() { - GoogleSignInAccount? user = _currentUser; + final GoogleSignInAccount? user = _currentUser; if (user != null) { return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -121,11 +124,11 @@ class SignInDemoState extends State { title: Text(user.displayName ?? ''), subtitle: Text(user.email), ), - const Text("Signed in successfully."), + 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'), @@ -137,10 +140,10 @@ class SignInDemoState extends State { return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - const Text("You are not currently signed in."), + 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 0379b9065333..822f83895cfb 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Example of Google Sign-In plugin. publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" dependencies: flutter: @@ -20,11 +20,10 @@ dependencies: dev_dependencies: espresso: ^0.1.0+2 - pedantic: ^1.10.0 - integration_test: - sdk: flutter flutter_driver: sdk: flutter + integration_test: + sdk: flutter flutter: uses-material-design: true 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.modulemap b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap deleted file mode 100644 index 271f509e7fd7..000000000000 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap +++ /dev/null @@ -1,10 +0,0 @@ -framework module google_sign_in { - umbrella header "google_sign_in-umbrella.h" - - export * - module * { export * } - - explicit module Test { - header "FLTGoogleSignInPlugin_Test.h" - } -} 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/ios/Classes/google_sign_in-umbrella.h b/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h deleted file mode 100644 index 343c390f1782..000000000000 --- a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h +++ /dev/null @@ -1,9 +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 - -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/google_sign_in.podspec deleted file mode 100644 index a0b73276fafa..000000000000 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ /dev/null @@ -1,26 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'google_sign_in' - s.version = '0.0.1' - s.summary = 'Google Sign-In plugin for Flutter' - s.description = <<-DESC -Enables Google Sign-In in Flutter apps. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/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_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.static_framework = true - - s.platform = :ios, '8.0' - - # GoogleSignIn ~> 5.0 does not support arm64 simulators. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } -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 04d60fbc7d21..a1f8f7bc49ca 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,8 +3,8 @@ // 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; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; @@ -12,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'; @@ -28,6 +29,7 @@ class GoogleSignInAuthentication { String? get accessToken => _data.accessToken; /// Server auth code used to access Google Login + @Deprecated('Use the `GoogleSignInAccount.serverAuthCode` property instead') String? get serverAuthCode => _data.serverAuthCode; @override @@ -38,12 +40,14 @@ class GoogleSignInAuthentication { /// [GoogleSignInUserData]. /// /// [id] is guaranteed to be non-null. +@immutable class GoogleSignInAccount implements GoogleIdentity { GoogleSignInAccount._(this._googleSignIn, GoogleSignInUserData data) : displayName = data.displayName, email = data.email, id = data.id, photoUrl = data.photoUrl, + serverAuthCode = data.serverAuthCode, _idToken = data.idToken { assert(id != null); } @@ -68,6 +72,9 @@ class GoogleSignInAccount implements GoogleIdentity { @override final String? photoUrl; + @override + final String? serverAuthCode; + final String? _idToken; final GoogleSignIn _googleSignIn; @@ -94,9 +101,8 @@ class GoogleSignInAccount implements GoogleIdentity { // On Android, there isn't an API for refreshing the idToken, so re-use // the one we obtained on login. - if (response.idToken == null) { - response.idToken = _idToken; - } + response.idToken ??= _idToken; + return GoogleSignInAuthentication._(response); } @@ -107,10 +113,10 @@ class GoogleSignInAccount implements GoogleIdentity { Future> get authHeaders async { final String? token = (await authentication).accessToken; return { - "Authorization": "Bearer $token", + 'Authorization': 'Bearer $token', // TODO(kevmoo): Use the correct value once it's available from authentication // See https://github.com/flutter/flutter/issues/80905 - "X-Goog-AuthUser": "0", + 'X-Goog-AuthUser': '0', }; } @@ -124,19 +130,25 @@ class GoogleSignInAccount implements GoogleIdentity { } @override - bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! GoogleSignInAccount) return false; + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! GoogleSignInAccount) { + return false; + } final GoogleSignInAccount otherAccount = other; return displayName == otherAccount.displayName && email == otherAccount.email && id == otherAccount.id && photoUrl == otherAccount.photoUrl && + serverAuthCode == otherAccount.serverAuthCode && _idToken == otherAccount._idToken; } @override - int get hashCode => hashValues(displayName, email, id, photoUrl, _idToken); + int get hashCode => + Object.hash(displayName, email, id, photoUrl, _idToken, serverAuthCode); @override String toString() { @@ -145,6 +157,7 @@ class GoogleSignInAccount implements GoogleIdentity { 'email': email, 'id': id, 'photoUrl': photoUrl, + 'serverAuthCode': serverAuthCode }; return 'GoogleSignInAccount:$data'; } @@ -171,6 +184,7 @@ class GoogleSignIn { this.scopes = const [], this.hostedDomain, this.clientId, + this.serverClientId, }); /// Factory for creating default sign in user experience. @@ -216,10 +230,30 @@ 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; - StreamController _currentUserController = + /// 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; + + final StreamController _currentUserController = StreamController.broadcast(); /// Subscribe to this stream to be notified when the current user changes. @@ -248,15 +282,18 @@ 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, + )) + ..catchError((dynamic _) { + // Invalidate initialization if it errors out. + _initialization = null; + }); } /// The most recently scheduled method call. @@ -268,7 +305,7 @@ class GoogleSignIn { final Completer completer = Completer(); future.whenComplete(completer.complete).catchError((dynamic _) { // Ignore if previous call completed with an error. - // TODO: Should we log errors here, if debug or similar? + // TODO(ditman): Should we log errors here, if debug or similar? }); return completer.future; } diff --git a/packages/google_sign_in/google_sign_in/lib/src/common.dart b/packages/google_sign_in/google_sign_in/lib/src/common.dart index 068403e74629..8a1d4dcb354f 100644 --- a/packages/google_sign_in/google_sign_in/lib/src/common.dart +++ b/packages/google_sign_in/google_sign_in/lib/src/common.dart @@ -34,4 +34,7 @@ abstract class GoogleIdentity { /// /// Not guaranteed to be present for all users, even when configured. String? get photoUrl; + + /// Server auth code used to access Google Login + String? get serverAuthCode; } diff --git a/packages/google_sign_in/google_sign_in/lib/testing.dart b/packages/google_sign_in/google_sign_in/lib/testing.dart index c4d2da3089a5..e519b34b199a 100644 --- a/packages/google_sign_in/google_sign_in/lib/testing.dart +++ b/packages/google_sign_in/google_sign_in/lib/testing.dart @@ -67,6 +67,7 @@ class FakeUser { this.email, this.displayName, this.photoUrl, + this.serverAuthCode, this.idToken, this.accessToken, }); @@ -83,6 +84,9 @@ class FakeUser { /// Will be converted into [GoogleSignInUserData.photoUrl]. final String? photoUrl; + /// Will be converted into [GoogleSignInUserData.serverAuthCode]. + final String? serverAuthCode; + /// Will be converted into [GoogleSignInTokenData.idToken]. final String? idToken; @@ -94,6 +98,7 @@ class FakeUser { 'email': email, 'displayName': displayName, 'photoUrl': photoUrl, + 'serverAuthCode': serverAuthCode, 'idToken': idToken, }; } 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 18f9973454f6..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. @@ -106,10 +108,22 @@ class GoogleUserCircleAvatar extends StatelessWidget { FadeInImage.memoryNetwork( // This creates a transparent placeholder image, so that // [placeholder] shows through. - placeholder: Uint8List((size.round() * size.round())), + placeholder: _transparentImage, image: sizedPhotoUrl, ) ]), )); } } + +/// This is an transparent 1x1 gif image. +/// +/// Those bytes come from `resources/transparentImage.gif`. +final Uint8List _transparentImage = Uint8List.fromList( + [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x21, 0xf9, 0x04, 0x01, 0x00, // + 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, // + 0x00, 0x02, 0x01, 0x44, 0x00, 0x3B + ], +); diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 79009373c5d1..c7724adcebad 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -1,33 +1,35 @@ name: google_sign_in description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in +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.1.0 +version: 5.4.0 + environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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.0.1 + google_sign_in_android: ^6.0.0 + google_sign_in_ios: ^5.4.0 + google_sign_in_platform_interface: ^2.2.0 google_sign_in_web: ^0.10.0 - meta: ^1.3.0 dev_dependencies: + build_runner: ^2.1.10 flutter_driver: sdk: flutter flutter_test: @@ -35,4 +37,12 @@ dev_dependencies: http: ^0.13.0 integration_test: sdk: flutter - pedantic: ^1.10.0 + mockito: ^5.1.0 + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/android/app/google-services.json + - /example/ios/Runner/GoogleService-Info.plist + - /example/ios/RunnerTests/GoogleSignInTests.m + - /example/lib/main.dart + - /example/web/index.html diff --git a/packages/google_sign_in/google_sign_in/resources/README.md b/packages/google_sign_in/google_sign_in/resources/README.md new file mode 100644 index 000000000000..b3f0383a6695 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/resources/README.md @@ -0,0 +1,7 @@ +`transparentImage.gif` is a 1x1 transparent gif which comes from [this wikimedia page](https://commons.wikimedia.org/wiki/File:Transparent.gif): + +![](transparentImage.gif) + +This is the image used a placeholder for the `GoogleCircleAvatar` widget. + +The variable `_transparentImage` in `lib/widgets.dart` is the list of bytes of `transparentImage.gif`. \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in/resources/transparentImage.gif b/packages/google_sign_in/google_sign_in/resources/transparentImage.gif new file mode 100644 index 000000000000..f191b280ce91 Binary files /dev/null and b/packages/google_sign_in/google_sign_in/resources/transparentImage.gif differ diff --git a/packages/google_sign_in/google_sign_in/test/fife_test.dart b/packages/google_sign_in/google_sign_in/test/fife_test.dart index c81454ef0a8c..5b0524771eb8 100644 --- a/packages/google_sign_in/google_sign_in/test/fife_test.dart +++ b/packages/google_sign_in/google_sign_in/test/fife_test.dart @@ -15,17 +15,17 @@ void main() { const String expected = '$base/s20-c/photo.jpg'; test('with directives, sets size', () { - final String url = '$base/s64-c/photo.jpg'; + const String url = '$base/s64-c/photo.jpg'; expect(addSizeDirectiveToUrl(url, size), expected); }); test('no directives, sets size and crop', () { - final String url = '$base/photo.jpg'; + const String url = '$base/photo.jpg'; expect(addSizeDirectiveToUrl(url, size), expected); }); test('no crop, sets size and crop', () { - final String url = '$base/s64/photo.jpg'; + const String url = '$base/s64/photo.jpg'; expect(addSizeDirectiveToUrl(url, size), expected); }); }); @@ -36,29 +36,29 @@ void main() { const String expected = '$base=c-s20'; test('with directives, sets size', () { - final String url = '$base=s120-c'; + const String url = '$base=s120-c'; expect(addSizeDirectiveToUrl(url, size), expected); }); test('no directives, sets size and crop', () { - final String url = base; + const String url = base; expect(addSizeDirectiveToUrl(url, size), expected); }); test('no directives, but with an equals sign, sets size and crop', () { - final String url = '$base='; + const String url = '$base='; expect(addSizeDirectiveToUrl(url, size), expected); }); test('no crop, adds crop', () { - final String url = '$base=s120'; + const String url = '$base=s120'; expect(addSizeDirectiveToUrl(url, size), expected); }); test('many directives, sets size and crop, preserves other directives', () { - final String url = '$base=s120-c-fSoften=1,50,0'; - final String expected = '$base=c-fSoften=1,50,0-s20'; + const String url = '$base=s120-c-fSoften=1,50,0'; + const String expected = '$base=c-fSoften=1,50,0-s20'; expect(addSizeDirectiveToUrl(url, size), expected); }); }); 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 444edc4336ce..b8676bda298e 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,227 +4,191 @@ import 'dart:async'; -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/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", - }; - - 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 { - final fakeClientId = 'fakeClientId'; - googleSignIn = GoogleSignIn(clientId: fakeClientId); + test('clientId parameter is forwarded to implementation', () async { + const String fakeClientId = '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('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(), @@ -232,20 +196,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(), @@ -253,181 +213,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(auth.serverAuthCode, '789'); - 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 result = await googleSignIn.requestScopes(['testScope']); + 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", - ); - - 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.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/test/widgets_test.dart b/packages/google_sign_in/google_sign_in/test/widgets_test.dart new file mode 100644 index 000000000000..b847bc6de36e --- /dev/null +++ b/packages/google_sign_in/google_sign_in/test/widgets_test.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +/// A instantiable class that extends [GoogleIdentity] +class _TestGoogleIdentity extends GoogleIdentity { + _TestGoogleIdentity({ + required this.id, + required this.email, + this.photoUrl, + }); + + @override + final String id; + @override + final String email; + + @override + final String? photoUrl; + + @override + String? get displayName => null; + + @override + String? get serverAuthCode => null; +} + +/// A mocked [HttpClient] which always returns a [_MockHttpRequest]. +class _MockHttpClient extends Fake implements HttpClient { + @override + bool autoUncompress = true; + + @override + Future getUrl(Uri url) { + return Future.value(_MockHttpRequest()); + } +} + +/// A mocked [HttpClientRequest] which always returns a [_MockHttpClientResponse]. +class _MockHttpRequest extends Fake implements HttpClientRequest { + @override + Future close() { + return Future.value(_MockHttpResponse()); + } +} + +/// Arbitrary valid image returned by the [_MockHttpResponse]. +/// +/// This is an transparent 1x1 gif image. +/// It doesn't have to match the placeholder used in [GoogleUserCircleAvatar]. +/// +/// Those bytes come from `resources/transparentImage.gif`. +final Uint8List _transparentImage = Uint8List.fromList( + [ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x21, 0xf9, 0x04, 0x01, 0x00, // + 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, // + 0x00, 0x02, 0x01, 0x44, 0x00, 0x3B + ], +); + +/// A mocked [HttpClientResponse] which is empty and has a [statusCode] of 200 +/// and returns valid image. +class _MockHttpResponse extends Fake implements HttpClientResponse { + final Stream _delegate = + Stream.value(_transparentImage); + + @override + int get contentLength => -1; + + @override + HttpClientResponseCompressionState get compressionState { + return HttpClientResponseCompressionState.decompressed; + } + + @override + StreamSubscription listen(void Function(Uint8List event)? onData, + {Function? onError, void Function()? onDone, bool? cancelOnError}) { + return _delegate.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + int get statusCode => 200; +} + +void main() { + testWidgets('It should build the GoogleUserCircleAvatar successfully', + (WidgetTester tester) async { + final GoogleIdentity identity = _TestGoogleIdentity( + email: 'email@email.com', + id: 'userId', + photoUrl: 'photoUrl', + ); + tester.binding.window.physicalSizeTestValue = const Size(100, 100); + + await HttpOverrides.runZoned( + () async { + await tester.pumpWidget(MaterialApp( + home: SizedBox( + height: 100, + width: 100, + child: GoogleUserCircleAvatar( + identity: identity, + ), + ), + )); + }, + createHttpClient: (SecurityContext? c) => _MockHttpClient(), + ); + }); +} diff --git a/packages/battery/battery/AUTHORS b/packages/google_sign_in/google_sign_in_android/AUTHORS similarity index 100% rename from packages/battery/battery/AUTHORS rename to packages/google_sign_in/google_sign_in_android/AUTHORS 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..9852a1f733ae --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -0,0 +1,29 @@ +## 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/connectivity/connectivity_for_web/LICENSE b/packages/google_sign_in/google_sign_in_android/LICENSE similarity index 100% rename from packages/connectivity/connectivity_for_web/LICENSE rename to packages/google_sign_in/google_sign_in_android/LICENSE 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/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle new file mode 100644 index 000000000000..aeef6d6e14e3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -0,0 +1,55 @@ +group 'io.flutter.plugins.googlesignin' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.1' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation 'com.google.android.gms:play-services-auth:20.0.1' + implementation 'com.google.guava:guava:28.1-android' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:3.9.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 3a63f785aa9f..21640233f210 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,9 @@ 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"); + delegate.init( + result, signInOption, requestedScopes, hostedDomain, clientId, serverClientId); break; case METHOD_SIGN_IN_SILENTLY: @@ -183,7 +187,7 @@ public void onMethodCall(MethodCall call, Result result) { /** * A delegate interface that exposes all of the sign-in functionality for other plugins to use. - * The below {@link #Delegate} implementation should be used by any clients unless they need to + * The below {@link Delegate} implementation should be used by any clients unless they need to * override some of these functions, such as for testing. */ public interface IDelegate { @@ -193,7 +197,8 @@ public void init( String signInOption, List requestedScopes, String hostedDomain, - String clientId); + String clientId, + String serverClientId); /** * Returns the account information for the user who is signed in to this app. If no user is @@ -267,6 +272,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 +288,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 +325,8 @@ public void init( String signInOption, List requestedScopes, String hostedDomain, - String clientId) { + String clientId, + String serverClientId) { try { GoogleSignInOptions.Builder optionsBuilder; @@ -335,20 +343,38 @@ 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()); + // 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)) { - optionsBuilder.requestIdToken(clientId); - optionsBuilder.requestServerAuthCode(clientId); - } else if (clientIdIdentifier != 0) { - optionsBuilder.requestIdToken(context.getString(clientIdIdentifier)); - optionsBuilder.requestServerAuthCode(context.getString(clientIdIdentifier)); + if (Strings.isNullOrEmpty(serverClientId)) { + Log.w( + "google_sing_in", + "clientId is not supported on Android and is interpreted as serverClientId." + + "Use serverClientId instead to suppress this warning."); + serverClientId = clientId; + } else { + Log.w("google_sing_in", "clientId is not supported on Android and is ignored."); + } + } + + 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); } for (String scope : requestedScopes) { optionsBuilder.requestScopes(new Scope(scope)); @@ -358,7 +384,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/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 new file mode 100644 index 000000000000..11f8cda2e9b1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -0,0 +1,270 @@ +// Copyright 2013 The Flutter 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.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; +import static org.mockito.Mockito.when; + +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; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +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; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +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 + public void setUp() { + MockitoAnnotations.initMocks(this); + 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); + } + + @Test + public void requestScopes_ResultErrorIfAccountIsNull() { + MethodCall methodCall = new MethodCall("requestScopes", null); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + plugin.onMethodCall(methodCall, result); + verify(result).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test + public void requestScopes_ResultTrueIfAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); + + plugin.onMethodCall(methodCall, result); + verify(result).success(true); + } + + @Test + public void requestScopes_RequestsPermissionIfNotGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + + verify(mockGoogleSignIn) + .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); + } + + @Test + public void requestScopes_ReturnsFalseIfPermissionDenied() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, + Activity.RESULT_CANCELED, + new Intent()); + + verify(result).success(false); + } + + @Test + public void requestScopes_ReturnsTrueIfPermissionGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test(expected = IllegalStateException.class) + public void signInThrowsWithoutActivity() { + final GoogleSignInPlugin plugin = new GoogleSignInPlugin(); + plugin.initInstance( + mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class)); + + 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); + } + + 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()); + } + + private static MethodCall buildInitMethodCall(String clientId, String serverClientId) { + return buildInitMethodCall( + "SignInOption.standard", Collections.emptyList(), clientId, serverClientId); + } + + private static MethodCall buildInitMethodCall( + String signInOption, List scopes, String clientId, String serverClientId) { + 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); + } + 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..8eb153eb8efd --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/README.md @@ -0,0 +1,3 @@ +# google_sign_in_android example + +Exercises the Android implementation of `GoogleSignInPlatform`. 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/android_intent/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 similarity index 100% rename from packages/android_intent/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java 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/android_intent/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 similarity index 100% rename from packages/android_intent/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/android_intent/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 similarity index 100% rename from packages/android_intent/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/android_intent/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 similarity index 100% rename from packages/android_intent/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/android_intent/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 similarity index 100% rename from packages/android_intent/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/android_intent/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 similarity index 100% rename from packages/android_intent/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/google_sign_in/google_sign_in_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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/android_alarm_manager/example/android/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle similarity index 100% rename from packages/android_alarm_manager/example/android/build.gradle rename to packages/google_sign_in/google_sign_in_android/example/android/build.gradle 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/android_intent/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/android_intent/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/android_intent/example/android/settings.gradle b/packages/google_sign_in/google_sign_in_android/example/android/settings.gradle similarity index 100% rename from packages/android_intent/example/android/settings.gradle rename to packages/google_sign_in/google_sign_in_android/example/android/settings.gradle 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..4d12bbad7987 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -0,0 +1,30 @@ +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.8.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.1.0+2 + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart similarity index 100% rename from packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart rename to packages/google_sign_in/google_sign_in_android/example/test_driver/integration_test.dart 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..d96328de695a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.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: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 channel.invokeMethod('init', { + 'signInOption': signInOption.toString(), + 'scopes': scopes, + 'hostedDomain': hostedDomain, + 'clientId': 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}) { + 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..0a863dce1714 --- /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.0.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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..7d39ae5f0232 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -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. + +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', + }), + () { + 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/battery/battery_platform_interface/AUTHORS b/packages/google_sign_in/google_sign_in_ios/AUTHORS similarity index 100% rename from packages/battery/battery_platform_interface/AUTHORS rename to packages/google_sign_in/google_sign_in_ios/AUTHORS 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..5ed38de5cb74 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -0,0 +1,25 @@ +## 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/connectivity/connectivity_macos/LICENSE b/packages/google_sign_in/google_sign_in_ios/LICENSE similarity index 100% rename from packages/connectivity/connectivity_macos/LICENSE rename to packages/google_sign_in/google_sign_in_ios/LICENSE 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..04c3372dc3b0 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/README.md @@ -0,0 +1,3 @@ +# google_sign_in_ios example + +Exercises the iOS implementation of `GoogleSignInPlatform`. 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/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 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/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/connectivity/connectivity_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/package_info/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/package_info/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.h b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/AppDelegate.h rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.h diff --git a/packages/device_info/device_info/example/ios/Runner/AppDelegate.m b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/AppDelegate.m rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/AppDelegate.m diff --git a/packages/battery/battery/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 similarity index 100% rename from packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/connectivity/connectivity/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 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/battery/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/battery/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/connectivity/connectivity/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Base.lproj/Main.storyboard 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 82% 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 index 52d8da1b5964..c8fa27864b43 100644 --- 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 @@ -6,7 +6,7 @@ @import XCTest; @interface GoogleSignInUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @implementation GoogleSignInUITests @@ -19,9 +19,9 @@ - (void)setUp { } - (void)testSignInPopUp { - XCUIApplication* app = self.app; + XCUIApplication *app = self.app; - XCUIElement* signInButton = app.buttons[@"SIGN IN"]; + XCUIElement *signInButton = app.buttons[@"SIGN IN"]; if (![signInButton waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Sign In button"); @@ -34,9 +34,9 @@ - (void)testSignInPopUp { - (void)allowSignInPermissions { // The "Sign In" system permissions pop up isn't caught by // addUIInterruptionMonitorWithDescription. - XCUIApplication* springboard = + XCUIApplication *springboard = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; - XCUIElement* permissionAlert = springboard.alerts.firstMatch; + XCUIElement *permissionAlert = springboard.alerts.firstMatch; if ([permissionAlert waitForExistenceWithTimeout:5.0]) { [permissionAlert.buttons[@"Continue"] tap]; } else { 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..d17c929a989f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +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.8.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 + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/device_info/device_info/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart similarity index 100% rename from packages/device_info/device_info/example/test_driver/integration_test.dart rename to packages/google_sign_in/google_sign_in_ios/example/test_driver/integration_test.dart diff --git a/packages/battery/battery/ios/Assets/.gitkeep b/packages/google_sign_in/google_sign_in_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/battery/battery/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/ios/Classes/FLTGoogleSignInPlugin.modulemap b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap new file mode 100644 index 000000000000..31e30d93c582 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.modulemap @@ -0,0 +1,10 @@ +framework module google_sign_in_ios { + umbrella header "google_sign_in_ios-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FLTGoogleSignInPlugin_Test.h" + } +} 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/ios/Classes/google_sign_in_ios-umbrella.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.h new file mode 100644 index 000000000000..23b7e992a5cd --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/google_sign_in_ios-umbrella.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 + +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/ios/google_sign_in_ios.podspec b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec new file mode 100644 index 000000000000..4e307098fd6d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'google_sign_in_ios' + s.version = '0.0.1' + s.summary = 'Google Sign-In plugin for Flutter' + s.description = <<-DESC +Enables Google Sign-In in Flutter apps. + DESC + 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/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', '~> 6.2' + s.static_framework = true + s.platform = :ios, '9.0' + 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..ce8865664507 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -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 '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, + }) { + if (signInOption == SignInOption.games) { + throw PlatformException( + code: 'unsupported-options', + message: 'Games sign in is not supported on iOS'); + } + return channel.invokeMethod('init', { + 'scopes': scopes, + 'hostedDomain': hostedDomain, + 'clientId': 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..65c8928c1402 --- /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.4.0 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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..92637e938fd9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -0,0 +1,151 @@ +// Copyright 2013 The Flutter Authors. 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.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 ee43db685339..591f36eb1ae8 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,25 @@ +## 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. + +## 2.1.1 + +* Removes dependency on `meta`. + +## 2.1.0 + +* Add serverAuthCode attribute to user data + ## 2.0.1 * Updates `init` function in `MethodChannelGoogleSignIn` to parametrize `clientId` property. 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 42038879e90b..69d8455b6bd2 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart' show visibleForTesting; + +import 'package:flutter/foundation.dart' show visibleForTesting; + import 'src/method_channel_google_sign_in.dart'; import 'src/types.dart'; @@ -61,8 +63,7 @@ abstract class GoogleSignInPlatform { /// 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 @@ -87,6 +88,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 23c35ac240b9..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 @@ -4,11 +4,10 @@ import 'dart:async'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show visibleForTesting; 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, }); } @@ -55,7 +65,7 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { .invokeMapMethod('getTokens', { 'email': email, 'shouldRecoverAuth': shouldRecoverAuth, - }).then((result) => getTokenDataFromMap(result!)); + }).then((Map? result) => getTokenDataFromMap(result!)); } @override 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 61231d1b70b9..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. @@ -31,6 +90,7 @@ class GoogleSignInUserData { this.displayName, this.photoUrl, this.idToken, + this.serverAuthCode, }); /// The display name of the signed in user. @@ -66,20 +126,32 @@ class GoogleSignInUserData { /// data. String? idToken; + /// Server auth code used to access Google Login + String? serverAuthCode; + @override - int get hashCode => - hashObjects([displayName, email, id, photoUrl, idToken]); + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => hashObjects( + [displayName, email, id, photoUrl, idToken, serverAuthCode]); @override - bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! GoogleSignInUserData) return false; + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! GoogleSignInUserData) { + return false; + } final GoogleSignInUserData otherUserData = other; return otherUserData.displayName == displayName && otherUserData.email == email && otherUserData.id == id && otherUserData.photoUrl == photoUrl && - otherUserData.idToken == idToken; + otherUserData.idToken == idToken && + otherUserData.serverAuthCode == serverAuthCode; } } @@ -102,12 +174,20 @@ class GoogleSignInTokenData { String? serverAuthCode; @override + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => hash3(idToken, accessToken, serverAuthCode); @override - bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! GoogleSignInTokenData) return false; + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! GoogleSignInTokenData) { + return false; + } final GoogleSignInTokenData otherTokenData = other; return otherTokenData.idToken == idToken && otherTokenData.accessToken == accessToken && diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart index 4a70ec4d25ef..6f03a6c357fe 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart @@ -10,18 +10,19 @@ GoogleSignInUserData? getUserDataFromMap(Map? data) { return null; } return GoogleSignInUserData( - email: data['email']!, - id: data['id']!, - displayName: data['displayName'], - photoUrl: data['photoUrl'], - idToken: data['idToken']); + 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'], - accessToken: data['accessToken'], - serverAuthCode: data['serverAuthCode'], + 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_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index 75b3d98b562d..9ad3e1cf005b 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 @@ -1,23 +1,21 @@ name: google_sign_in_platform_interface description: A common platform interface for the google_sign_in plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_platform_interface 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.0.1 +version: 2.2.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - meta: ^1.3.0 quiver: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 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 b3ac51b7fa52..bf960abc7375 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 @@ -2,14 +2,17 @@ // 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'; 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'; void main() { + // Store the initial instance before any tests change it. + final GoogleSignInPlatform initialInstance = GoogleSignInPlatform.instance; + group('$GoogleSignInPlatform', () { test('$MethodChannelGoogleSignIn is the default instance', () { - expect(GoogleSignInPlatform.instance, isA()); + expect(initialInstance, isA()); }); test('Cannot be implemented with `implements`', () { @@ -26,6 +29,44 @@ void main() { GoogleSignInPlatform.instance = ImplementsWithIsMock(); }); }); + + 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 { 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 390c12583a79..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,14 +5,15 @@ 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 = { - "email": "john.doe@gmail.com", - "id": "8162538176523816253123", - "photoUrl": "https://lh5.googleusercontent.com/photo.jpg", - "displayName": "John Doe", + 'email': 'john.doe@gmail.com', + 'id': '8162538176523816253123', + 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', + 'displayName': 'John Doe', + 'idToken': '123', + 'serverAuthCode': '789', }; const Map kTokenData = { @@ -33,7 +34,7 @@ const Map kDefaultResponses = { }; final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); -final GoogleSignInTokenData? kToken = +final GoogleSignInTokenData kToken = getTokenDataFromMap(kTokenData as Map); void main() { @@ -106,6 +107,8 @@ void main() { 'scopes': ['two', 'scopes'], 'signInOption': 'SignInOption.games', 'clientId': 'fakeClientId', + 'serverClientId': null, + 'forceCodeForRefreshToken': false, }), () { googleSignIn.getTokens( @@ -120,18 +123,40 @@ void main() { 'token': 'abc', }), () { - googleSignIn.requestScopes(['newScope', 'anotherScope']); + googleSignIn.requestScopes(['newScope', 'anotherScope']); }: isMethodCall('requestScopes', arguments: { - 'scopes': ['newScope', 'anotherScope'], + 'scopes': ['newScope', 'anotherScope'], }), googleSignIn.signOut: isMethodCall('signOut', arguments: null), googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), }; - tests.keys.forEach((Function f) => f()); + for (final Function f in tests.keys) { + f(); + } 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 556f69524026..12e6d9630f9c 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,35 @@ +## 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. + +## 0.10.0+4 + +* Removes dependency on `meta`. + ## 0.10.0+3 * Updated URL to the `google_sign_in` package in README. 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/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..12f8f2f3f167 --- /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 e1a97cee6cf7..81d9f1489a23 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'; @@ -15,10 +17,10 @@ import 'src/test_utils.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - GoogleSignInTokenData expectedTokenData = + final GoogleSignInTokenData expectedTokenData = GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); - GoogleSignInUserData expectedUserData = GoogleSignInUserData( + final GoogleSignInUserData expectedUserData = GoogleSignInUserData( displayName: 'Foo Bar', email: 'foo@example.com', id: '123', @@ -28,49 +30,49 @@ 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') as String; + final String code = js_util.getProperty(e, 'code'); expect(code, 'idpiframe_initialization_failed'); } }); }); - 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... + 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 } @@ -99,7 +101,7 @@ void main() { }); testWidgets('requestScopes', (WidgetTester tester) async { await _discardInit(); - await expectLater(plugin.requestScopes(['newScope']), + await expectLater(plugin.requestScopes(['newScope']), throwsA(isA())); }); }); @@ -112,51 +114,84 @@ 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; }); testWidgets('signInSilently', (WidgetTester tester) async { - GoogleSignInUserData actualUser = (await plugin.signInSilently())!; + final GoogleSignInUserData actualUser = + (await plugin.signInSilently())!; expect(actualUser, expectedUserData); }); testWidgets('signIn', (WidgetTester tester) async { - GoogleSignInUserData actualUser = (await plugin.signIn())!; + final GoogleSignInUserData actualUser = (await plugin.signIn())!; expect(actualUser, expectedUserData); }); testWidgets('getTokens', (WidgetTester tester) async { - GoogleSignInTokenData actualToken = + final GoogleSignInTokenData actualToken = await plugin.getTokens(email: expectedUserData.email); expect(actualToken, expectedTokenData); }); testWidgets('requestScopes', (WidgetTester tester) async { - bool scopeGranted = await plugin.requestScopes(['newScope']); + final bool scopeGranted = + await plugin.requestScopes(['newScope']); expect(scopeGranted, isTrue); }); @@ -168,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; }); @@ -187,7 +222,7 @@ void main() { await plugin.signIn(); fail('plugin.signIn() should have thrown an exception!'); } catch (e) { - final String code = js_util.getProperty(e, 'code') as String; + final String code = js_util.getProperty(e, 'code'); expect(code, 'popup_closed_by_user'); } }); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart 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 2a085ccf3588..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(() => { @@ -95,7 +97,7 @@ GapiAuth2.prototype.getAuthInstance = function () { return new Promise((resolve, reject) => { window.setTimeout(() => { reject({ - error: '${error}' + error: '$error' }); }, 30); }); 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 10415204570c..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,18 +5,21 @@ 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 { @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_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml index e370ecc561d2..1bdb2f09c465 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.8.0" dependencies: flutter: @@ -12,11 +12,11 @@ dependencies: path: ../ dev_dependencies: - http: ^0.13.0 - js: ^0.6.3 flutter_driver: sdk: flutter flutter_test: sdk: flutter + http: ^0.13.0 integration_test: sdk: flutter + js: ^0.6.3 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 f40b42b1881e..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 @@ -5,11 +5,11 @@ import 'dart:async'; import 'dart:html' as html; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:js/js.dart'; -import 'package:meta/meta.dart'; import 'src/generated/gapiauth2.dart' as auth2; import 'src/load_gapi.dart' as gapi; @@ -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.', + ); } } @@ -55,7 +57,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @visibleForTesting Future get initialized { _assertIsInitCalled(); - return Future.wait([_isGapiInitialized, _isAuthInitialized]); + return Future.wait(>[_isGapiInitialized, _isAuthInitialized]); } String? _autoDetectedClientId; @@ -71,37 +73,51 @@ 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', )); - Completer isAuthInitialized = Completer(); + final Completer isAuthInitialized = Completer(); _isAuthInitialized = isAuthInitialized.future; _isInitCalled = true; auth.then(allowInterop((auth2.GoogleAuth initializedAuth) { // onSuccess - // TODO: https://github.com/flutter/flutter/issues/48528 + // TODO(ditman): https://github.com/flutter/flutter/issues/48528 // This plugin doesn't notify the app of external changes to the // state of the authentication, i.e: if you logout elsewhere... @@ -124,7 +140,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { await initialized; return gapiUserToPluginUserData( - await auth2.getAuthInstance()?.currentUser?.get()); + auth2.getAuthInstance()?.currentUser?.get()); } @override @@ -169,7 +185,9 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); - if (currentUser == null) return; + if (currentUser == null) { + return; + } return currentUser.disconnect(); } @@ -181,7 +199,9 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); - if (currentUser == null) return false; + if (currentUser == null) { + return false; + } return currentUser.isSignedIn(); } @@ -197,17 +217,22 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future requestScopes(List scopes) async { await initialized; - final currentUser = auth2.getAuthInstance()?.currentUser?.get(); + final auth2.GoogleUser? currentUser = + auth2.getAuthInstance()?.currentUser?.get(); - if (currentUser == null) return false; + if (currentUser == null) { + return false; + } - final grantedScopes = currentUser.getGrantedScopes() ?? ''; - final missingScopes = - scopes.where((scope) => !grantedScopes.contains(scope)); + final String grantedScopes = currentUser.getGrantedScopes() ?? ''; + final Iterable missingScopes = + scopes.where((String scope) => !grantedScopes.contains(scope)); - if (missingScopes.isEmpty) return true; + if (missingScopes.isEmpty) { + return true; + } - final response = await currentUser + final Object? response = await currentUser .grant(auth2.SigninOptions(scope: missingScopes.join(' '))); return response != null; diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart index 1e2db0fe4609..a6d5b9d8dbbb 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart @@ -10,7 +10,7 @@ // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi -// ignore_for_file: public_member_api_docs, unused_element +// ignore_for_file: public_member_api_docs, unused_element, sort_constructors_first, prefer_generic_function_type_aliases @JS() library gapi; 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 d5efc71d469a..e1721668f41f 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 +// ignore_for_file: public_member_api_docs, unused_element, non_constant_identifier_names, sort_constructors_first, always_specify_types @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/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart index 6d8c566f0412..f60d6cd57e56 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart @@ -7,8 +7,8 @@ library gapi_onload; import 'dart:async'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:js/js.dart'; -import 'package:meta/meta.dart'; import 'generated/gapi.dart' as gapi; import 'utils.dart' show injectJSLibraries; @@ -37,8 +37,10 @@ Future inject(String url, {List libraries = const []}) { }); // Attach the onload callback to the main url - final List allLibraries = [_addOnloadToScript(url)] - ..addAll(libraries); + final List allLibraries = [ + _addOnloadToScript(url), + ...libraries + ]; return Future.wait( >[injectJSLibraries(allLibraries), gapiOnLoad.future]); 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 bcfefc2054b4..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 @@ -25,15 +25,16 @@ Future injectJSLibraries( final html.Element targetElement = target ?? html.querySelector('head')!; - libraries.forEach((String library) { + for (final String library in libraries) { final html.ScriptElement script = html.ScriptElement() ..async = true ..defer = true + // ignore: unsafe_html ..src = library; - // TODO add a timeout race to fail this future + // TODO(ditman): add a timeout race to fail this future loading.add(script.onLoad.first); tags.add(script); - }); + } targetElement.children.addAll(tags); return Future.wait(loading); 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 723dbe9ce56f..1dedd6de6666 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -1,13 +1,13 @@ name: google_sign_in_web description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. -repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_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+3 +version: 0.10.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -22,11 +22,9 @@ 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 - meta: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/image_picker/analysis_options.yaml b/packages/image_picker/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/image_picker/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 4c89be1c3e48..36a47b1a3d42 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,63 @@ +## 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. + +## 0.8.4+9 + +* Internal code cleanup for stricter analysis options. + +## 0.8.4+8 + +* Configures the `UIImagePicker` to default to gallery instead of camera when +picking multiple images on pre-iOS 14 devices. + +## 0.8.4+7 + +* Refactors unit test to expose private interface via a separate test header instead of the inline declaration. + +## 0.8.4+6 + +* Fixes minor type issues in iOS implementation. + +## 0.8.4+5 + +* Improves the documentation on handling MainActivity being killed by the Android OS. +* Updates Android compileSdkVersion to 31. +* Fix iOS RunnerUITests search paths. + +## 0.8.4+4 + +* Fix typos in README.md. + +## 0.8.4+3 + +* Suppress a unchecked cast build warning. + ## 0.8.4+2 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index d8f5835fd402..2fa20be34859 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). @@ -26,7 +30,11 @@ 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. +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 +MainActivity destruction on Android](#handling-mainactivity-destruction-on-android) +section. It is no longer required to add `android:requestLegacyExternalStorage="true"` as an attribute to the `` tag in AndroidManifest.xml, as `image_picker` has been updated to make use of scoped storage. @@ -47,7 +55,7 @@ import 'package:image_picker/image_picker.dart'; // Pick a video final XFile? image = await _picker.pickVideo(source: ImageSource.gallery); // Capture a video - final XFile? photo = await _picker.pickVideo(source: ImageSource.camera); + final XFile? video = await _picker.pickVideo(source: ImageSource.camera); // Pick multiple images final List? images = await _picker.pickMultiImage(); ... @@ -55,7 +63,14 @@ import 'package:image_picker/image_picker.dart'; ### Handling MainActivity destruction on Android -Android system -- although very rarely -- sometimes kills the MainActivity after the image_picker finishes. When this happens, we lost the data selected from the image_picker. You can use `retrieveLostData` to retrieve the lost data in this situation. For example: +When under high memory pressure the Android system may kill the MainActivity of +the application using the image_picker. On Android the image_picker makes use +of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` +intents. This means that while the intent is executing the source application +is moved to the background and becomes eligable for cleanup when the system is +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 Future getLostData() async { @@ -65,7 +80,7 @@ Future getLostData() async { return; } if (response.files != null) { - for(final XFile file in response.files) { + for (final XFile file in response.files) { _handleFile(file); } } else { @@ -74,7 +89,10 @@ Future getLostData() async { } ``` -There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. +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+ @@ -87,4 +105,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/build.gradle b/packages/image_picker/image_picker/android/build.gradle deleted file mode 100755 index 1e6439e6a4eb..000000000000 --- a/packages/image_picker/image_picker/android/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -group 'io.flutter.plugins.imagepicker' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - 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" - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} 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/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java deleted file mode 100644 index 577675bd433a..000000000000 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ /dev/null @@ -1,329 +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.imagepicker; - -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleOwner; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; -import java.io.File; - -@SuppressWarnings("deprecation") -public class ImagePickerPlugin - implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { - - private class LifeCycleObserver - implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { - private final Activity thisActivity; - - LifeCycleObserver(Activity activity) { - this.thisActivity = activity; - } - - @Override - public void onCreate(@NonNull LifecycleOwner owner) {} - - @Override - public void onStart(@NonNull LifecycleOwner owner) {} - - @Override - public void onResume(@NonNull LifecycleOwner owner) {} - - @Override - public void onPause(@NonNull LifecycleOwner owner) {} - - @Override - public void onStop(@NonNull LifecycleOwner owner) { - onActivityStopped(thisActivity); - } - - @Override - public void onDestroy(@NonNull LifecycleOwner owner) { - onActivityDestroyed(thisActivity); - } - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} - - @Override - public void onActivityStarted(Activity activity) {} - - @Override - public void onActivityResumed(Activity activity) {} - - @Override - public void onActivityPaused(Activity activity) {} - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} - - @Override - public void onActivityDestroyed(Activity activity) { - if (thisActivity == activity && activity.getApplicationContext() != null) { - ((Application) activity.getApplicationContext()) - .unregisterActivityLifecycleCallbacks( - this); // Use getApplicationContext() to avoid casting failures - } - } - - @Override - public void onActivityStopped(Activity activity) { - if (thisActivity == activity) { - delegate.saveStateBeforeResult(); - } - } - } - - 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 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; - - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - if (registrar.activity() == null) { - // If a background flutter view tries to register the plugin, there will be no activity from the registrar, - // we stop the registering process immediately because the ImagePicker requires an activity. - return; - } - Activity activity = registrar.activity(); - Application application = null; - if (registrar.context() != null) { - application = (Application) (registrar.context().getApplicationContext()); - } - ImagePickerPlugin plugin = new ImagePickerPlugin(); - plugin.setup(registrar.messenger(), application, activity, registrar, null); - } - - /** - * Default constructor for the plugin. - * - *

Use this constructor for production code. - */ - // See also: * {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. - public ImagePickerPlugin() {} - - @VisibleForTesting - ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) { - this.delegate = delegate; - this.activity = activity; - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - pluginBinding = binding; - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - pluginBinding = null; - } - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - activityBinding = binding; - setup( - pluginBinding.getBinaryMessenger(), - (Application) pluginBinding.getApplicationContext(), - activityBinding.getActivity(), - null, - activityBinding); - } - - @Override - public void onDetachedFromActivity() { - tearDown(); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity(); - } - - @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - onAttachedToActivity(binding); - } - - private void setup( - final BinaryMessenger messenger, - final Application application, - 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); - } - } - - 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; - } - - @VisibleForTesting - final ImagePickerDelegate constructDelegate(final Activity setupActivity) { - final ImagePickerCache cache = new ImagePickerCache(setupActivity); - - final File externalFilesDirectory = setupActivity.getCacheDir(); - final ExifDataCopier exifDataCopier = new ExifDataCopier(); - final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); - return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache); - } - - // MethodChannel.Result wrapper that responds on the platform thread. - private static class MethodResultWrapper implements MethodChannel.Result { - private MethodChannel.Result methodResult; - private Handler handler; - - MethodResultWrapper(MethodChannel.Result result) { - methodResult = result; - handler = new Handler(Looper.getMainLooper()); - } - - @Override - public void success(final Object result) { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.success(result); - } - }); - } - - @Override - public void error( - final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.error(errorCode, errorMessage, errorDetails); - } - }); - } - - @Override - public void notImplemented() { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.notImplemented(); - } - }); - } - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { - if (activity == null) { - rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null); - return; - } - MethodChannel.Result result = new MethodResultWrapper(rawResult); - int imageSource; - if (call.argument("cameraDevice") != null) { - CameraDevice device; - int deviceIntValue = call.argument("cameraDevice"); - if (deviceIntValue == CAMERA_DEVICE_FRONT) { - device = CameraDevice.FRONT; - } else { - device = CameraDevice.REAR; - } - delegate.setCameraDevice(device); - } - switch (call.method) { - case METHOD_CALL_IMAGE: - imageSource = call.argument("source"); - switch (imageSource) { - case SOURCE_GALLERY: - delegate.chooseImageFromGallery(call, result); - break; - case SOURCE_CAMERA: - delegate.takeImageWithCamera(call, result); - break; - default: - throw new IllegalArgumentException("Invalid image source: " + imageSource); - } - break; - case METHOD_CALL_MULTI_IMAGE: - delegate.chooseMultiImageFromGallery(call, result); - break; - case METHOD_CALL_VIDEO: - imageSource = call.argument("source"); - switch (imageSource) { - case SOURCE_GALLERY: - delegate.chooseVideoFromGallery(call, result); - break; - case SOURCE_CAMERA: - delegate.takeVideoWithCamera(call, result); - break; - default: - throw new IllegalArgumentException("Invalid video source: " + imageSource); - } - break; - case METHOD_CALL_RETRIEVE: - delegate.retrieveLostImage(result); - break; - default: - throw new IllegalArgumentException("Unknown method " + call.method); - } - } -} 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.iml b/packages/image_picker/image_picker/example/android.iml deleted file mode 100755 index 462b903e05b6..000000000000 --- a/packages/image_picker/image_picker/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - 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 f7fbaae4c9fd..e83cb5a13c06 100755 --- a/packages/image_picker/image_picker/example/android/app/build.gradle +++ b/packages/image_picker/image_picker/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 29 + compileSdkVersion 31 testOptions.unitTests.includeAndroidResources = true lintOptions { @@ -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/image_picker_example.iml b/packages/image_picker/image_picker/example/image_picker_example.iml deleted file mode 100755 index 1ae40a0f7f54..000000000000 --- a/packages/image_picker/image_picker/example/image_picker_example.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/image_picker/image_picker/example/ios/Podfile b/packages/image_picker/image_picker/example/ios/Podfile index 8979c25fea5e..f7d6a5e68c3a 100644 --- a/packages/image_picker/image_picker/example/ios/Podfile +++ b/packages/image_picker/image_picker/example/ios/Podfile @@ -29,16 +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 - target 'RunnerUITests' do - inherit! :search_paths - 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 192962839b24..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 @@ -7,45 +7,16 @@ 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 */; }; - 56E9C6956BC15C647C89EB23 /* libPods-RunnerUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A908FAEEA2A9B26D903C09C5 /* libPods-RunnerUITests.a */; }; 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 */; }; 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; @@ -61,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 = ""; }; - 15BE72415096DFE5D077E563 /* 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 = ""; }; - 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 = ""; }; - 515A7EC9B4C971C01E672CF8 /* 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 = ""; }; 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 = ""; }; 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; }; @@ -91,34 +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 = ""; }; - A908FAEEA2A9B26D903C09C5 /* libPods-RunnerUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; - BE7AEE7026403C46006181AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - BE7AEE7826403CC8006181AA /* 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 = ( - 56E9C6956BC15C647C89EB23 /* libPods-RunnerUITests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -130,23 +69,10 @@ /* 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 */, - 334733F62668136400DCC49E /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; 680049282280E33D006DD6AB /* TestImages */ = { isa = PBXGroup; children = ( + 86E9A88F272747B90017E6E0 /* webpImage.webp */, 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */, 680049362280F2B8006DD6AB /* jpgImage.jpg */, 680049352280F2B8006DD6AB /* pngImage.png */, @@ -154,23 +80,11 @@ 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 */, - 15BE72415096DFE5D077E563 /* Pods-RunnerUITests.debug.xcconfig */, - 515A7EC9B4C971C01E672CF8 /* Pods-RunnerUITests.release.xcconfig */, DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */, 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */, ); @@ -194,9 +108,6 @@ 680049282280E33D006DD6AB /* TestImages */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 334733F32668136400DCC49E /* RunnerTests */, - 6801C8372555D726009DAF8D /* RunnerUITests */, - BE7AEE6D26403C46006181AA /* RunnerUITestiOS14 */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -207,8 +118,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, - 334733F22668136400DCC49E /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -237,20 +146,10 @@ name = "Supporting Files"; sourceTree = ""; }; - BE7AEE6D26403C46006181AA /* RunnerUITestiOS14 */ = { - isa = PBXGroup; - children = ( - BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */, - BE7AEE7026403C46006181AA /* Info.plist */, - ); - path = RunnerUITestiOS14; - sourceTree = ""; - }; CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { isa = PBXGroup; children = ( EC32F6993F4529982D9519F1 /* libPods-Runner.a */, - A908FAEEA2A9B26D903C09C5 /* libPods-RunnerUITests.a */, 35AE65F25E0B8C8214D8372B /* libPods-RunnerTests.a */, ); name = Frameworks; @@ -259,44 +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 = ( - 4F8C1F500AF4DCAB62651A1E /* [CP] Check Pods Manifest.lock */, - 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" */; @@ -325,19 +186,9 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1100; + 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 = { @@ -362,30 +213,11 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 334733F12668136400DCC49E /* RunnerTests */, - 6801C8352555D726009DAF8D /* RunnerUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 334733F02668136400DCC49E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - 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; @@ -414,28 +246,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 4F8C1F500AF4DCAB62651A1E /* [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; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -468,52 +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 */, - 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; @@ -526,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; @@ -559,83 +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; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - 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; - DEVELOPMENT_TEAM = ""; - 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; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - 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; - DEVELOPMENT_TEAM = ""; - 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 = { @@ -747,7 +424,6 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -769,7 +445,6 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -789,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/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b100e5cd18d7..9b24f28c25cc 100755 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ #import "AppDelegate.h" -int main(int argc, char* argv[]) { +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/example/ios/RunnerTests/ImagePickerPluginTests.m deleted file mode 100644 index cc901f084071..000000000000 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m +++ /dev/null @@ -1,258 +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 "ImagePickerTestImages.h" - -@import image_picker; -@import XCTest; -#import - -@interface MockViewController : UIViewController -@property(nonatomic, retain) UIViewController *mockPresented; -@end - -@implementation MockViewController -@synthesize mockPresented; - -- (UIViewController *)presentedViewController { - return mockPresented; -} - -@end - -@interface FLTImagePickerPlugin (Test) -@property(copy, nonatomic) FlutterResult result; -- (void)handleSavedPathList:(NSMutableArray *)pathList; -- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; -@end - -@interface ImagePickerPluginTests : XCTestCase -@property(readonly, nonatomic) id mockUIImagePicker; -@property(readonly, nonatomic) id mockAVCaptureDevice; -@end - -@implementation ImagePickerPluginTests - -- (void)setUp { - _mockUIImagePicker = OCMClassMock([UIImagePickerController class]); - _mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); -} - -- (void)testPluginPickImageDeviceBack { - // UIImagePickerControllerSourceTypeCamera is supported - OCMStub(ClassMethod( - [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) - .andReturn(YES); - - // UIImagePickerControllerCameraDeviceRear is supported - OCMStub(ClassMethod( - [_mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) - .andReturn(YES); - - // AVAuthorizationStatusAuthorized is supported - OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusAuthorized); - - // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceRear); -} - -- (void)testPluginPickImageDeviceFront { - // UIImagePickerControllerSourceTypeCamera is supported - OCMStub(ClassMethod( - [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) - .andReturn(YES); - - // UIImagePickerControllerCameraDeviceFront is supported - OCMStub(ClassMethod([_mockUIImagePicker - isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) - .andReturn(YES); - - // AVAuthorizationStatusAuthorized is supported - OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusAuthorized); - - // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceFront); -} - -- (void)testPluginPickVideoDeviceBack { - // UIImagePickerControllerSourceTypeCamera is supported - OCMStub(ClassMethod( - [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) - .andReturn(YES); - - // UIImagePickerControllerCameraDeviceRear is supported - OCMStub(ClassMethod( - [_mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) - .andReturn(YES); - - // AVAuthorizationStatusAuthorized is supported - OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusAuthorized); - - // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceRear); -} - -- (void)testPluginPickVideoDeviceFront { - // UIImagePickerControllerSourceTypeCamera is supported - OCMStub(ClassMethod( - [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) - .andReturn(YES); - - // UIImagePickerControllerCameraDeviceFront is supported - OCMStub(ClassMethod([_mockUIImagePicker - isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) - .andReturn(YES); - - // AVAuthorizationStatusAuthorized is supported - OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusAuthorized); - - // Run test - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceFront); -} - -#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)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - plugin.result = ^(id result) { - - }; - // To ensure the flow does not crash by multiple cancel call - [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; - [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; -} - -#pragma mark - Test video duration - -- (void)testPickingVideoWithDuration { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95); -} - -- (void)testViewController { - UIWindow *window = [UIWindow new]; - MockViewController *vc1 = [MockViewController new]; - window.rootViewController = vc1; - - UIViewController *vc2 = [UIViewController new]; - vc1.mockPresented = vc2; - - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - 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]]; - - 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]; - - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); - - XCTAssertEqualObjects(pickImageResult.code, @"create_error"); -} - -- (void)testPluginMultiImagePathHasItem { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSString *savedPath = @"test"; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:savedPath]; - - 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]; - - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); - - XCTAssertEqual(pickImageResult, pathList); -} - -@end diff --git a/packages/image_picker/image_picker/example/ios/TestImages/webpImage.webp b/packages/image_picker/image_picker/example/ios/TestImages/webpImage.webp new file mode 100644 index 000000000000..ab7d40d83968 Binary files /dev/null and b/packages/image_picker/image_picker/example/ios/TestImages/webpImage.webp differ diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 0f5ba76db6df..4eecc5fa2a1a 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -13,13 +13,15 @@ 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 MaterialApp( + return const MaterialApp( title: 'Image Picker Demo', home: MyHomePage(title: 'Image Picker Example'), ); @@ -27,19 +29,19 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, this.title}) : super(key: key); + const MyHomePage({Key? key, this.title}) : super(key: key); final String? title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { List? _imageFileList; - set _imageFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + void _setImageFileListFromFile(XFile? value) { + _imageFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -69,7 +71,7 @@ class _MyHomePageState extends State { // Mute the video so it auto-plays in web! // This is not needed if the call to .play is the result of user // interaction (clicking on a "play" button, for example). - final double volume = kIsWeb ? 0.0 : 1.0; + const double volume = kIsWeb ? 0.0 : 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -78,7 +80,7 @@ class _MyHomePageState extends State { } } - void _onImageButtonPressed(ImageSource source, + Future _onImageButtonPressed(ImageSource source, {BuildContext? context, bool isMultiImage = false}) async { if (_controller != null) { await _controller!.setVolume(0.0); @@ -91,7 +93,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final pickedFileList = await _picker.pickMultiImage( + final List? pickedFileList = await _picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, @@ -109,14 +111,14 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final pickedFile = await _picker.pickImage( + final XFile? pickedFile = await _picker.pickImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, 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: (context, 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 { @@ -358,28 +364,30 @@ class _MyHomePageState extends State { BuildContext context, OnPickImageCallback onPick) async { return showDialog( context: context, - builder: (context) { + builder: (BuildContext context) { return AlertDialog( - title: Text('Add optional parameters'), + title: const Text('Add optional parameters'), content: Column( children: [ TextField( controller: maxWidthController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: - InputDecoration(hintText: "Enter maxWidth if desired"), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), ), TextField( controller: maxHeightController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: - InputDecoration(hintText: "Enter maxHeight if desired"), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), ), TextField( controller: qualityController, keyboardType: TextInputType.number, - decoration: - InputDecoration(hintText: "Enter quality if desired"), + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), ), ], ), @@ -393,13 +401,13 @@ class _MyHomePageState extends State { TextButton( child: const Text('PICK'), onPressed: () { - double? width = maxWidthController.text.isNotEmpty + final double? width = maxWidthController.text.isNotEmpty ? double.parse(maxWidthController.text) : null; - double? height = maxHeightController.text.isNotEmpty + final double? height = maxHeightController.text.isNotEmpty ? double.parse(maxHeightController.text) : null; - int? quality = qualityController.text.isNotEmpty + final int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; onPick(width, height, quality); @@ -411,11 +419,11 @@ class _MyHomePageState extends State { } } -typedef void OnPickImageCallback( +typedef OnPickImageCallback = void Function( double? maxWidth, double? maxHeight, int? quality); class AspectRatioVideo extends StatefulWidget { - 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 e11da82d5da8..23c682af3922 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -4,10 +4,9 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" dependencies: - video_player: ^2.1.4 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 @@ -18,14 +17,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: ../ + video_player: ^2.1.4 dev_dependencies: - espresso: ^0.1.0+2 + espresso: ^0.2.0 flutter_driver: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m deleted file mode 100644 index cf3103195482..000000000000 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ /dev/null @@ -1,590 +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 "FLTImagePickerPlugin.h" - -#import -#import -#import -#import -#import -#import - -#import "FLTImagePickerImageUtil.h" -#import "FLTImagePickerMetaDataUtil.h" -#import "FLTImagePickerPhotoAssetUtil.h" -#import "FLTPHPickerSaveImageToPathOperation.h" - -@interface FLTImagePickerPlugin () - -@property(copy, nonatomic) FlutterResult result; - -@property(assign, nonatomic) int maxImagesAllowed; - -@property(copy, nonatomic) NSDictionary *arguments; - -@property(strong, nonatomic) PHPickerViewController *pickerViewController API_AVAILABLE(ios(14)); - -@end - -static const int SOURCE_CAMERA = 0; -static const int SOURCE_GALLERY = 1; - -typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPickerClassType }; - -@implementation FLTImagePickerPlugin { - UIImagePickerController *_imagePickerController; -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker" - binaryMessenger:[registrar messenger]]; - FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (UIImagePickerController *)getImagePickerController { - return _imagePickerController; -} - -- (UIViewController *)viewControllerWithWindow:(UIWindow *)window { - UIWindow *windowToUse = window; - if (windowToUse == nil) { - for (UIWindow *window in [UIApplication sharedApplication].windows) { - if (window.isKeyWindow) { - windowToUse = window; - break; - } - } - } - - UIViewController *topController = windowToUse.rootViewController; - while (topController.presentedViewController) { - topController = topController.presentedViewController; - } - return topController; -} - -/** - * Returns the UIImagePickerControllerCameraDevice to use given [arguments]. - * - * 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. - */ -- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments { - NSInteger cameraDevice = [[arguments objectForKey:@"cameraDevice"] intValue]; - return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; -} - -- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { - PHPickerConfiguration *config = - [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; - config.selectionLimit = maxImagesAllowed; // Setting to zero allow us to pick unlimited photos - config.filter = [PHPickerFilter imagesFilter]; - - _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; - _pickerViewController.delegate = self; - _pickerViewController.presentationController.delegate = self; - - self.maxImagesAllowed = maxImagesAllowed; - - [self checkPhotoAuthorizationForAccessLevel]; -} - -- (void)pickImageWithUIImagePicker { - _imagePickerController = [[UIImagePickerController alloc] init]; - _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - _imagePickerController.delegate = self; - _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; - - int imageSource = [[_arguments objectForKey:@"source"] intValue]; - - self.maxImagesAllowed = 1; - - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorization]; - break; - case SOURCE_GALLERY: - [self checkPhotoAuthorization]; - break; - default: - self.result([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; - } - - if ([@"pickImage" isEqualToString:call.method]) { - self.result = result; - _arguments = call.arguments; - int imageSource = [[_arguments objectForKey:@"source"] intValue]; - - 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 pickImageWithUIImagePicker]; - } - } else { - [self pickImageWithUIImagePicker]; - } - } else if ([@"pickMultiImage" isEqualToString:call.method]) { - if (@available(iOS 14, *)) { - self.result = result; - _arguments = call.arguments; - [self pickImageWithPHPicker:0]; - } else { - [self pickImageWithUIImagePicker]; - } - } else if ([@"pickVideo" isEqualToString:call.method]) { - _imagePickerController = [[UIImagePickerController alloc] init]; - _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - _imagePickerController.delegate = self; - _imagePickerController.mediaTypes = @[ - (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, - (NSString *)kUTTypeMPEG4 - ]; - _imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; - - self.result = result; - _arguments = call.arguments; - - int imageSource = [[_arguments objectForKey:@"source"] intValue]; - if ([[_arguments objectForKey:@"maxDuration"] isKindOfClass:[NSNumber class]]) { - NSTimeInterval max = [[_arguments objectForKey:@"maxDuration"] doubleValue]; - _imagePickerController.videoMaximumDuration = max; - } - - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorization]; - break; - case SOURCE_GALLERY: - [self checkPhotoAuthorization]; - break; - default: - result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid video source." - details:nil]); - break; - } - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)showCamera { - @synchronized(self) { - if (_imagePickerController.beingPresented) { - return; - } - } - UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments]; - // Camera is not available on simulators - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && - [UIImagePickerController isCameraDeviceAvailable:device]) { - _imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; - _imagePickerController.cameraDevice = device; - [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController - animated:YES - completion:nil]; - } else { - UIAlertController *cameraErrorAlert = [UIAlertController - alertControllerWithTitle:NSLocalizedString(@"Error", @"Alert title when camera unavailable") - message:NSLocalizedString(@"Camera not available.", - "Alert message when camera unavailable") - preferredStyle:UIAlertControllerStyleAlert]; - [cameraErrorAlert - addAction:[UIAlertAction actionWithTitle:NSLocalizedString( - @"OK", @"Alert button when camera unavailable") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action){ - }]]; - [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert - animated:YES - completion:nil]; - self.result(nil); - self.result = nil; - _arguments = nil; - } -} - -- (void)checkCameraAuthorization { - AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; - - switch (status) { - case AVAuthorizationStatusAuthorized: - [self showCamera]; - break; - case AVAuthorizationStatusNotDetermined: { - [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo - completionHandler:^(BOOL granted) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (granted) { - [self showCamera]; - } else { - [self errorNoCameraAccess:AVAuthorizationStatusDenied]; - } - }); - }]; - break; - } - case AVAuthorizationStatusDenied: - case AVAuthorizationStatusRestricted: - default: - [self errorNoCameraAccess:status]; - break; - } -} - -- (void)checkPhotoAuthorization { - PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; - switch (status) { - case PHAuthorizationStatusNotDetermined: { - [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (status == PHAuthorizationStatusAuthorized) { - [self showPhotoLibrary:UIImagePickerClassType]; - } else { - [self errorNoPhotoAccess:status]; - } - }); - }]; - break; - } - case PHAuthorizationStatusAuthorized: - [self showPhotoLibrary:UIImagePickerClassType]; - break; - case PHAuthorizationStatusDenied: - case PHAuthorizationStatusRestricted: - default: - [self errorNoPhotoAccess:status]; - break; - } -} - -- (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { - PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; - switch (status) { - case PHAuthorizationStatusNotDetermined: { - [PHPhotoLibrary - requestAuthorizationForAccessLevel:PHAccessLevelReadWrite - handler:^(PHAuthorizationStatus status) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (status == PHAuthorizationStatusAuthorized) { - [self showPhotoLibrary:PHPickerClassType]; - } else if (status == PHAuthorizationStatusLimited) { - [self showPhotoLibrary:PHPickerClassType]; - } else { - [self errorNoPhotoAccess:status]; - } - }); - }]; - break; - } - case PHAuthorizationStatusAuthorized: - case PHAuthorizationStatusLimited: - [self showPhotoLibrary:PHPickerClassType]; - break; - case PHAuthorizationStatusDenied: - case PHAuthorizationStatusRestricted: - default: - [self errorNoPhotoAccess:status]; - break; - } -} - -- (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]); - break; - case AVAuthorizationStatusDenied: - default: - self.result([FlutterError errorWithCode:@"camera_access_denied" - message:@"The user did not allow camera access." - details:nil]); - break; - } -} - -- (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]); - break; - case PHAuthorizationStatusDenied: - default: - self.result([FlutterError errorWithCode:@"photo_access_denied" - message:@"The user did not allow photo access." - details:nil]); - break; - } -} - -- (void)showPhotoLibrary:(ImagePickerClassType)imagePickerClassType { - // No need to check if SourceType is available. It always is. - switch (imagePickerClassType) { - case PHPickerClassType: - [[self viewControllerWithWindow:nil] presentViewController:_pickerViewController - animated:YES - completion:nil]; - break; - case UIImagePickerClassType: - _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController - animated:YES - completion:nil]; - break; - } -} - -- (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { - if (![imageQuality isKindOfClass:[NSNumber class]]) { - imageQuality = @1; - } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) { - imageQuality = @1; - } else { - imageQuality = @([imageQuality floatValue] / 100); - } - return imageQuality; -} - -- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } -} - -- (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; - } - return; - } - dispatch_queue_t backgroundQueue = - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); - dispatch_async(backgroundQueue, ^{ - NSNumber *maxWidth = [self->_arguments objectForKey:@"maxWidth"]; - NSNumber *maxHeight = [self->_arguments objectForKey:@"maxHeight"]; - NSNumber *imageQuality = [self->_arguments objectForKey:@"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; - }]; - [operationQueue addOperation:operation]; - } - [operationQueue waitUntilAllOperationsAreFinished]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleSavedPathList:pathList]; - }); - }); -} - -/** - * Creates an NSMutableArray of a certain size filled with NSNull objects. - * - * The difference with initWithCapacity is that initWithCapacity still gives an empty array making - * it impossible to add objects on an index larger than the size. - * - * @param size The length of the required array - * @return NSMutableArray An array of a specified size - */ -- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { - NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size]; - for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++) - ; - return mutableArray; -} - -- (void)imagePickerController:(UIImagePickerController *)picker - didFinishPickingMediaWithInfo:(NSDictionary *)info { - NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL]; - [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; - // The method dismissViewControllerAnimated does not immediately prevent - // 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) { - return; - } - if (videoURL != nil) { - if (@available(iOS 13.0, *)) { - NSString *fileName = [videoURL lastPathComponent]; - NSURL *destination = - [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - - if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { - NSError *error; - if (![[videoURL path] isEqualToString:[destination path]]) { - [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; - - if (error) { - self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]); - self.result = nil; - return; - } - } - videoURL = destination; - } - } - self.result(videoURL.path); - self.result = nil; - _arguments = nil; - } else { - UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage]; - if (image == nil) { - image = [info objectForKey:UIImagePickerControllerOriginalImage]; - } - NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; - NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; - NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; - NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; - - PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; - - if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { - image = [FLTImagePickerImageUtil scaledImage:image - maxWidth:maxWidth - maxHeight:maxHeight - isMetadataAvailable:YES]; - } - - if (!originalAsset) { - // 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)imagePickerControllerDidCancel:(UIImagePickerController *)picker { - [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; - if (!self.result) { - return; - } - self.result(nil); - self.result = nil; - _arguments = nil; -} - -- (void)saveImageWithOriginalImageData:(NSData *)originalImageData - image:(UIImage *)image - maxWidth:(NSNumber *)maxWidth - maxHeight:(NSNumber *)maxHeight - imageQuality:(NSNumber *)imageQuality { - NSString *savedPath = - [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:originalImageData - image:image - maxWidth:maxWidth - maxHeight:maxHeight - imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; -} - -- (void)saveImageWithPickerInfo:(NSDictionary *)info - image:(UIImage *)image - imageQuality:(NSNumber *)imageQuality { - NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info - image:image - imageQuality:imageQuality]; - [self handleSavedPathList:@[ 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) { - 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]); - } - } else { - // This should never happen. - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList should not be nil" - details:nil]); - } - self.result = nil; - _arguments = nil; -} - -@end diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m deleted file mode 100644 index 30da22774d07..000000000000 --- a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ /dev/null @@ -1,132 +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 "FLTPHPickerSaveImageToPathOperation.h" - -API_AVAILABLE(ios(14)) -@interface FLTPHPickerSaveImageToPathOperation () - -@property(strong, nonatomic) PHPickerResult *result; -@property(assign, nonatomic) NSNumber *maxHeight; -@property(assign, nonatomic) NSNumber *maxWidth; -@property(assign, nonatomic) NSNumber *desiredImageQuality; - -@end - -typedef void (^GetSavedPath)(NSString *); - -@implementation FLTPHPickerSaveImageToPathOperation { - BOOL executing; - BOOL finished; - GetSavedPath getSavedPath; -} - -- (instancetype)initWithResult:(PHPickerResult *)result - maxHeight:(NSNumber *)maxHeight - maxWidth:(NSNumber *)maxWidth - desiredImageQuality:(NSNumber *)desiredImageQuality - savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { - if (self = [super init]) { - if (result) { - self.result = result; - self.maxHeight = maxHeight; - self.maxWidth = maxWidth; - self.desiredImageQuality = desiredImageQuality; - getSavedPath = savedPathBlock; - executing = NO; - finished = NO; - } else { - return nil; - } - return self; - } else { - return nil; - } -} - -- (BOOL)isConcurrent { - return YES; -} - -- (BOOL)isExecuting { - return executing; -} - -- (BOOL)isFinished { - return finished; -} - -- (void)setFinished:(BOOL)isFinished { - [self willChangeValueForKey:@"isFinished"]; - self->finished = isFinished; - [self didChangeValueForKey:@"isFinished"]; -} - -- (void)setExecuting:(BOOL)isExecuting { - [self willChangeValueForKey:@"isExecuting"]; - self->executing = isExecuting; - [self didChangeValueForKey:@"isExecuting"]; -} - -- (void)completeOperationWithPath:(NSString *)savedPath { - [self setExecuting:NO]; - [self setFinished:YES]; - getSavedPath(savedPath); -} - -- (void)start { - if ([self isCancelled]) { - [self setFinished:YES]; - return; - } - if (@available(iOS 14, *)) { - [self setExecuting:YES]; - [self.result.itemProvider - loadObjectOfClass:[UIImage class] - completionHandler:^(__kindof id _Nullable image, - NSError *_Nullable error) { - if ([image isKindOfClass:[UIImage class]]) { - __block UIImage *localImage = image; - PHAsset *originalAsset = - [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result]; - - if (self.maxWidth != (id)[NSNull null] || self.maxHeight != (id)[NSNull null]) { - localImage = [FLTImagePickerImageUtil scaledImage:localImage - maxWidth:self.maxWidth - maxHeight:self.maxHeight - isMetadataAvailable:originalAsset != nil]; - } - __block NSString *savedPath; - if (!originalAsset) { - // Image picked without an original asset (e.g. User pick image without permission) - savedPath = - [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil - image:localImage - imageQuality:self.desiredImageQuality]; - [self completeOperationWithPath:savedPath]; - } 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. - savedPath = [FLTImagePickerPhotoAssetUtil - saveImageWithOriginalImageData:imageData - image:localImage - maxWidth:self.maxWidth - maxHeight:self.maxHeight - imageQuality:self.desiredImageQuality]; - [self completeOperationWithPath:savedPath]; - }]; - } - } - }]; - } else { - [self setFinished:YES]; - } -} - -@end diff --git a/packages/image_picker/image_picker/ios/image_picker.podspec b/packages/image_picker/image_picker/ios/image_picker.podspec deleted file mode 100644 index 0d6cb0304723..000000000000 --- a/packages/image_picker/image_picker/ios/image_picker.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'image_picker' - s.version = '0.0.1' - s.summary = 'Flutter plugin that shows an image picker.' - s.description = <<-DESC -A Flutter plugin for picking images from the image library, and taking new pictures with the camera. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/image_picker' } - s.documentation_url = 'https://pub.dev/packages/image_picker' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } -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..84c649028c96 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -207,6 +207,17 @@ class ImagePicker { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { + 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.getImage( source: source, maxWidth: maxWidth, @@ -245,6 +256,17 @@ class ImagePicker { 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 platform.getMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index ba5ce6635ed6..acc085a06bb9 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -1,35 +1,36 @@ name: image_picker 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/master/packages/image_picker/image_picker +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+2 +version: 0.8.5+3 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.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_ios: ^0.8.4+11 image_picker_platform_interface: ^2.3.0 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 - pedantic: ^1.10.0 plugin_platform_interface: ^2.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 f295e3d02f66..b3db08020d7e 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 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,182 @@ 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, + maxWidth: null, + maxHeight: null, + imageQuality: null), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: null, + imageQuality: null), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: null, + maxHeight: 10.0, + imageQuality: null), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: null), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: null, + imageQuality: 70), + mockPlatform.pickImage( + source: ImageSource.camera, + maxWidth: null, + 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, + preferredCameraDevice: CameraDevice.rear)); }); 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( - 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.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.rear, + maxDuration: null), + mockPlatform.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.rear, + 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, + preferredCameraDevice: CameraDevice.rear)); }); 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 +276,26 @@ 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, - }), - ], - ); + verifyInOrder([ + mockPlatform.pickMultiImage( + maxWidth: null, maxHeight: null, imageQuality: null), + mockPlatform.pickMultiImage( + maxWidth: 10.0, maxHeight: null, imageQuality: null), + mockPlatform.pickMultiImage( + maxWidth: null, maxHeight: 10.0, imageQuality: null), + mockPlatform.pickMultiImage( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: null), + mockPlatform.pickMultiImage( + maxWidth: 10.0, maxHeight: null, imageQuality: 70), + mockPlatform.pickMultiImage( + maxWidth: null, maxHeight: 10.0, imageQuality: 70), + mockPlatform.pickMultiImage( + 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, - ); - }); - - 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 +304,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 10bc64082aca..f981195fe1b3 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,51 @@ 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 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.getImage( + source: anyNamed('source'), + maxWidth: anyNamed('maxWidth'), + maxHeight: anyNamed('maxHeight'), + imageQuality: anyNamed('imageQuality'), + preferredCameraDevice: anyNamed('preferredCameraDevice'))) + .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.getImage(source: ImageSource.camera), + mockPlatform.getImage(source: ImageSource.gallery), + ]); }); 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 +75,184 @@ 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.getImage( + source: ImageSource.camera, + maxWidth: null, + maxHeight: null, + imageQuality: null), + mockPlatform.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: null, + imageQuality: null), + mockPlatform.getImage( + source: ImageSource.camera, + maxWidth: null, + maxHeight: 10.0, + imageQuality: null), + mockPlatform.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: null), + mockPlatform.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: null, + imageQuality: 70), + mockPlatform.getImage( + source: ImageSource.camera, + maxWidth: null, + maxHeight: 10.0, + imageQuality: 70), + mockPlatform.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70), + ]); }); 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.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.rear)); }); 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.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front)); }); }); 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, + preferredCameraDevice: CameraDevice.rear, + maxDuration: null), + mockPlatform.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.rear, + 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, + preferredCameraDevice: CameraDevice.rear)); }); 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 +261,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.getMultiImage( + 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.pickMultiImage(); await picker.pickMultiImage( maxWidth: 10.0, @@ -401,62 +311,39 @@ 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.getMultiImage( + maxWidth: null, maxHeight: null, imageQuality: null), + mockPlatform.getMultiImage( + maxWidth: 10.0, maxHeight: null, imageQuality: null), + mockPlatform.getMultiImage( + maxWidth: null, maxHeight: 10.0, imageQuality: null), + mockPlatform.getMultiImage( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: null), + mockPlatform.getMultiImage( + maxWidth: 10.0, maxHeight: null, imageQuality: 70), + mockPlatform.getMultiImage( + maxWidth: null, maxHeight: 10.0, imageQuality: 70), + mockPlatform.getMultiImage( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70), + ]); }); 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 a null image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); expect(await picker.pickMultiImage(), isNull); expect(await picker.pickMultiImage(), isNull); @@ -465,7 +352,3 @@ void main() { }); }); } - -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..641a104a33c5 --- /dev/null +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -0,0 +1,136 @@ +// 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?>); +} diff --git a/packages/connectivity/connectivity/AUTHORS b/packages/image_picker/image_picker_android/AUTHORS similarity index 100% rename from packages/connectivity/connectivity/AUTHORS rename to packages/image_picker/image_picker_android/AUTHORS 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..664be36f880f --- /dev/null +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -0,0 +1,20 @@ +## 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/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle new file mode 100755 index 000000000000..393c4bc4b559 --- /dev/null +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -0,0 +1,62 @@ +group 'io.flutter.plugins.imagepicker' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + dependencies { + 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:3.10.0' + testImplementation 'androidx.test:core:1.4.0' + testImplementation "org.robolectric:robolectric:4.8.1" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} 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 99% 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 index a60c1f173041..dddf67e6a382 100644 --- 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 @@ -217,6 +217,7 @@ void saveStateBeforeResult() { void retrieveLostImage(MethodChannel.Result result) { Map resultMap = cache.getCacheMap(); + @SuppressWarnings("unchecked") ArrayList pathList = (ArrayList) resultMap.get(cache.MAP_KEY_PATH_LIST); ArrayList newPathList = new ArrayList<>(); if (pathList != null) { 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/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 new file mode 100644 index 000000000000..8336a145e93a --- /dev/null +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -0,0 +1,391 @@ +// Copyright 2013 The Flutter 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.imagepicker; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import java.io.File; + +@SuppressWarnings("deprecation") +public class ImagePickerPlugin + implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { + + private class LifeCycleObserver + implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { + private final Activity thisActivity; + + LifeCycleObserver(Activity activity) { + this.thisActivity = activity; + } + + @Override + public void onCreate(@NonNull LifecycleOwner owner) {} + + @Override + public void onStart(@NonNull LifecycleOwner owner) {} + + @Override + public void onResume(@NonNull LifecycleOwner owner) {} + + @Override + public void onPause(@NonNull LifecycleOwner owner) {} + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + onActivityStopped(thisActivity); + } + + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { + onActivityDestroyed(thisActivity); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityResumed(Activity activity) {} + + @Override + public void onActivityPaused(Activity activity) {} + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + + @Override + public void onActivityDestroyed(Activity activity) { + if (thisActivity == activity && activity.getApplicationContext() != null) { + ((Application) activity.getApplicationContext()) + .unregisterActivityLifecycleCallbacks( + this); // Use getApplicationContext() to avoid casting failures + } + } + + @Override + public void onActivityStopped(Activity activity) { + if (thisActivity == activity) { + 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_android"; + + private static final int SOURCE_CAMERA = 0; + private static final int SOURCE_GALLERY = 1; + + private FlutterPluginBinding pluginBinding; + private ActivityState activityState; + + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + if (registrar.activity() == null) { + // If a background flutter view tries to register the plugin, there will be no activity from the registrar, + // we stop the registering process immediately because the ImagePicker requires an activity. + return; + } + Activity activity = registrar.activity(); + Application application = null; + if (registrar.context() != null) { + application = (Application) (registrar.context().getApplicationContext()); + } + ImagePickerPlugin plugin = new ImagePickerPlugin(); + plugin.setup(registrar.messenger(), application, activity, registrar, null); + } + + /** + * Default constructor for the plugin. + * + *

Use this constructor for production code. + */ + // See also: * {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing. + public ImagePickerPlugin() {} + + @VisibleForTesting + ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) { + activityState = new ActivityState(delegate, activity); + } + + @VisibleForTesting + final ActivityState getActivityState() { + return activityState; + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + pluginBinding = binding; + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + pluginBinding = null; + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + setup( + pluginBinding.getBinaryMessenger(), + (Application) pluginBinding.getApplicationContext(), + binding.getActivity(), + null, + binding); + } + + @Override + public void onDetachedFromActivity() { + tearDown(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } + + private void setup( + final BinaryMessenger messenger, + final Application application, + final Activity activity, + final PluginRegistry.Registrar registrar, + final ActivityPluginBinding activityBinding) { + activityState = + new ActivityState(application, activity, messenger, this, registrar, activityBinding); + } + + private void tearDown() { + if (activityState != null) { + activityState.release(); + activityState = null; + } + } + + @VisibleForTesting + final ImagePickerDelegate constructDelegate(final Activity setupActivity) { + final ImagePickerCache cache = new ImagePickerCache(setupActivity); + + final File externalFilesDirectory = setupActivity.getCacheDir(); + final ExifDataCopier exifDataCopier = new ExifDataCopier(); + final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); + return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache); + } + + // MethodChannel.Result wrapper that responds on the platform thread. + private static class MethodResultWrapper implements MethodChannel.Result { + private MethodChannel.Result methodResult; + private Handler handler; + + MethodResultWrapper(MethodChannel.Result result) { + methodResult = result; + handler = new Handler(Looper.getMainLooper()); + } + + @Override + public void success(final Object result) { + handler.post( + new Runnable() { + @Override + public void run() { + methodResult.success(result); + } + }); + } + + @Override + public void error( + final String errorCode, final String errorMessage, final Object errorDetails) { + handler.post( + new Runnable() { + @Override + public void run() { + methodResult.error(errorCode, errorMessage, errorDetails); + } + }); + } + + @Override + public void notImplemented() { + handler.post( + new Runnable() { + @Override + public void run() { + methodResult.notImplemented(); + } + }); + } + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { + 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"); + if (deviceIntValue == CAMERA_DEVICE_FRONT) { + device = CameraDevice.FRONT; + } else { + device = CameraDevice.REAR; + } + delegate.setCameraDevice(device); + } + switch (call.method) { + case METHOD_CALL_IMAGE: + imageSource = call.argument("source"); + switch (imageSource) { + case SOURCE_GALLERY: + delegate.chooseImageFromGallery(call, result); + break; + case SOURCE_CAMERA: + delegate.takeImageWithCamera(call, result); + break; + default: + throw new IllegalArgumentException("Invalid image source: " + imageSource); + } + break; + case METHOD_CALL_MULTI_IMAGE: + delegate.chooseMultiImageFromGallery(call, result); + break; + case METHOD_CALL_VIDEO: + imageSource = call.argument("source"); + switch (imageSource) { + case SOURCE_GALLERY: + delegate.chooseVideoFromGallery(call, result); + break; + case SOURCE_CAMERA: + delegate.takeVideoWithCamera(call, result); + break; + default: + throw new IllegalArgumentException("Invalid video source: " + imageSource); + } + break; + case METHOD_CALL_RETRIEVE: + delegate.retrieveLostImage(result); + break; + default: + throw new IllegalArgumentException("Unknown method " + call.method); + } + } +} 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 82% 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..ce41343e3d2c 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,10 +5,13 @@ 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; @@ -16,6 +19,11 @@ 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); } @@ -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..16b5c51839f8 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/README.md @@ -0,0 +1,3 @@ +# image_picker_example + +Demonstrates how to use the `image_picker` plugin. 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/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java 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_android/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 new file mode 100644 index 000000000000..c4a1532d940c --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.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.imagepickerexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.imagepicker.ImagePickerPlugin; +import org.junit.Test; + +public class ImagePickerTest { + @Test + public void imagePickerPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(ImagePickerTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(ImagePickerPlugin.class)); + }); + } +} 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/battery/battery/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/battery/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/battery/battery/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/battery/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/battery/battery/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/battery/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/battery/battery/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/battery/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/battery/battery/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/battery/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/image_picker/image_picker_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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/battery/battery/example/android/settings.gradle b/packages/image_picker/image_picker_android/example/android/settings.gradle old mode 100644 new mode 100755 similarity index 100% rename from packages/battery/battery/example/android/settings.gradle rename to packages/image_picker/image_picker_android/example/android/settings.gradle 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..b5afb16235db --- /dev/null +++ b/packages/image_picker/image_picker_android/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.8.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 + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart b/packages/image_picker/image_picker_android/example/test_driver/integration_test.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart rename to packages/image_picker/image_picker_android/example/test_driver/integration_test.dart 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..8cbaaac71daf --- /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+1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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.3.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 d11ead3bb64e..b69ba597aca3 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,25 @@ +## 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. + +## 2.1.5 + +* Removes dependency on `meta`. + +## 2.1.4 + +* Implemented `maxWidth`, `maxHeight` and `imageQuality` when selecting images + (except for gifs). + ## 2.1.3 * Add `implements` to pubspec. diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 73f2dfc4b84f..c8b85f21cc89 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -43,7 +43,8 @@ Each browser may implement `capture` any way they please, so it may (or may not) difference in your users' experience. ### pickImage() -The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported on the web. +The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images. +The argument `imageQuality` only works for jpeg and webp images. ### pickVideo() The argument `maxDuration` is not supported on the web. @@ -63,7 +64,7 @@ You should be able to use `package:image_picker` _almost_ as normal. Once the user has picked a file, the returned `PickedFile` instance will contain a `network`-accessible URL (pointing to a location within the browser). -The instace will also let you retrieve the bytes of the selected file across all platforms. +The instance will also let you retrieve the bytes of the selected file across all platforms. If you want to use the path directly, your code would need look like this: diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index c1025a9f07d3..9fe40da2557c 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -11,16 +11,17 @@ import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; -final String expectedStringContents = 'Hello, world!'; -final String otherStringContents = 'Hello again, world!'; +const String expectedStringContents = 'Hello, world!'; +const String otherStringContents = 'Hello again, world!'; final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List; final Uint8List otherBytes = utf8.encode(otherStringContents) as Uint8List; -final Map options = { +final Map options = { 'type': 'text/plain', 'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch, }; -final html.File textFile = html.File([bytes], 'hello.txt', options); -final html.File secondTextFile = html.File([otherBytes], 'secondFile.txt'); +final html.File textFile = html.File([bytes], 'hello.txt', options); +final html.File secondTextFile = + html.File([otherBytes], 'secondFile.txt'); void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -33,16 +34,17 @@ void main() { }); testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async { - final mockInput = html.FileUploadInputElement(); + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - final overrides = ImagePickerPluginTestOverrides() - ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile]); - final plugin = ImagePickerPlugin(overrides: overrides); + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final file = plugin.pickFile(); + final Future file = plugin.pickFile(); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); @@ -54,16 +56,17 @@ void main() { }); testWidgets('Can select a file', (WidgetTester tester) async { - final mockInput = html.FileUploadInputElement(); + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - final overrides = ImagePickerPluginTestOverrides() - ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile]); - final plugin = ImagePickerPlugin(overrides: overrides); + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final image = plugin.getImage(source: ImageSource.camera); + final Future image = plugin.getImage(source: ImageSource.camera); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); @@ -85,16 +88,18 @@ void main() { }); testWidgets('Can select multiple files', (WidgetTester tester) async { - final mockInput = html.FileUploadInputElement(); + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - final overrides = ImagePickerPluginTestOverrides() - ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile, secondTextFile]); + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = + ((_) => [textFile, secondTextFile]); - final plugin = ImagePickerPlugin(overrides: overrides); + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final files = plugin.getMultiImage(); + final Future> files = plugin.getMultiImage(); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); @@ -135,7 +140,7 @@ void main() { group('createInputElement', () { testWidgets('accept: any, capture: null', (WidgetTester tester) async { - html.Element input = plugin.createInputElement('any', null); + final html.Element input = plugin.createInputElement('any', null); expect(input.attributes, containsPair('accept', 'any')); expect(input.attributes, isNot(contains('capture'))); @@ -143,7 +148,7 @@ void main() { }); testWidgets('accept: any, capture: something', (WidgetTester tester) async { - html.Element input = plugin.createInputElement('any', 'something'); + final html.Element input = plugin.createInputElement('any', 'something'); expect(input.attributes, containsPair('accept', 'any')); expect(input.attributes, containsPair('capture', 'something')); @@ -152,7 +157,7 @@ void main() { testWidgets('accept: any, capture: null, multi: true', (WidgetTester tester) async { - html.Element input = + final html.Element input = plugin.createInputElement('any', null, multiple: true); expect(input.attributes, containsPair('accept', 'any')); @@ -162,7 +167,7 @@ void main() { testWidgets('accept: any, capture: something, multi: true', (WidgetTester tester) async { - html.Element input = + final html.Element input = plugin.createInputElement('any', 'something', multiple: true); expect(input.attributes, containsPair('accept', 'any')); 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 new file mode 100644 index 000000000000..1efd7b29a810 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.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:async'; +import 'dart:html' as html; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_for_web/src/image_resizer.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +//This is a sample 10x10 png image +const String pngFileBase64Contents = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXqQzX+/v6lfubTAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAHEwAABxMBziAPCAAAAAd0SU1FB+UJHgsdDM0ErZoAAAALSURBVAjXY2DABwAAHgABboVHMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOS0zMFQxMToyOToxMi0wNDowMHCDC24AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDktMzBUMTE6Mjk6MTItMDQ6MDAB3rPSAAAAAElFTkSuQmCC'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Under test... + late ImageResizer imageResizer; + late XFile pngFile; + setUp(() { + imageResizer = ImageResizer(); + final html.File pngHtmlFile = + _base64ToFile(pngFileBase64Contents, 'pngImage.png'); + pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile), + name: pngHtmlFile.name, mimeType: pngHtmlFile.type); + }); + + 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); + }); + + testWidgets( + "canvas is loaded with image's width and height when max width and max height are null", + (WidgetTester widgetTester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); + final html.CanvasElement canvas = + imageResizer.resizeImageElement(imageElement, null, null); + expect(canvas.width, imageElement.width); + expect(canvas.height, imageElement.height); + }); + + testWidgets( + 'canvas size is scaled when max width and max height are not null', + (WidgetTester widgetTester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); + final html.CanvasElement canvas = + imageResizer.resizeImageElement(imageElement, 8, 8); + expect(canvas.width, 8); + expect(canvas.height, 8); + }); + + testWidgets('resized image is returned after converting canvas to file', + (WidgetTester widgetTester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); + final html.CanvasElement canvas = + imageResizer.resizeImageElement(imageElement, null, null); + final XFile resizedImage = + await imageResizer.writeCanvasToFile(pngFile, canvas, null); + expect(resizedImage.name, 'scaled_${pngFile.name}'); + }); + + testWidgets('image is scaled when maxWidth is set', + (WidgetTester tester) async { + final XFile scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, 5, null, null); + expect(scaledImage.name, 'scaled_${pngFile.name}'); + final Size scaledImageSize = await _getImageSize(scaledImage); + expect(scaledImageSize, const Size(5, 5)); + }); + + testWidgets('image is scaled when maxHeight is set', + (WidgetTester tester) async { + final XFile scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, null, 6, null); + expect(scaledImage.name, 'scaled_${pngFile.name}'); + final Size scaledImageSize = await _getImageSize(scaledImage); + expect(scaledImageSize, const Size(6, 6)); + }); + + testWidgets('image is scaled when imageQuality is set', + (WidgetTester tester) async { + final XFile scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, null, null, 89); + expect(scaledImage.name, 'scaled_${pngFile.name}'); + }); + + testWidgets('image is scaled when maxWidth,maxHeight,imageQuality are set', + (WidgetTester tester) async { + final XFile scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, 3, 4, 89); + expect(scaledImage.name, 'scaled_${pngFile.name}'); + }); + + testWidgets('image is not scaled when maxWidth,maxHeight, is set', + (WidgetTester tester) async { + final XFile scaledImage = + await imageResizer.resizeImageIfNeeded(pngFile, null, null, null); + expect(scaledImage.name, pngFile.name); + }); +} + +Future _getImageSize(XFile file) async { + final Completer completer = Completer(); + final html.ImageElement image = html.ImageElement(src: file.path); + image.onLoad.listen((html.Event event) { + completer.complete(Size(image.width!.toDouble(), image.height!.toDouble())); + }); + image.onError.listen((html.Event event) { + completer.complete(const Size(0, 0)); + }); + return completer.future; +} + +html.File _base64ToFile(String data, String fileName) { + final List arr = data.split(','); + final String bstr = html.window.atob(arr[1]); + int n = bstr.length; + final Uint8List u8arr = Uint8List(n); + + while (n >= 1) { + u8arr[n - 1] = bstr.codeUnitAt(n - 1); + n--; + } + + return html.File([u8arr], fileName); +} 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 e1a38dcdcd46..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,19 +5,22 @@ 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 { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); 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 8dadde267e8a..72316ee60988 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -1,21 +1,21 @@ -name: connectivity_for_web_integration_tests +name: image_picker_for_web_integration_tests publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.8.0" dependencies: - image_picker_for_web: - path: ../ flutter: sdk: flutter + image_picker_for_web: + path: ../ dev_dependencies: - js: ^0.6.3 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter + js: ^0.6.3 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 b170ee3256ab..88d439c5487f 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 @@ -5,31 +5,36 @@ import 'dart:async'; import 'dart:html' as html; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:meta/meta.dart'; +import 'package:image_picker_for_web/src/image_resizer.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; -final String _kAcceptImageMimeType = 'image/*'; -final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; +const String _kImagePickerInputsDomId = '__image_picker_web-file-input'; +const String _kAcceptImageMimeType = 'image/*'; +const String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; /// The web implementation of [ImagePickerPlatform]. /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { - final ImagePickerPluginTestOverrides? _overrides; - - bool get _hasOverrides => _overrides != null; - - late html.Element _target; - /// A constructor that allows tests to override the function that creates file inputs. ImagePickerPlugin({ @visibleForTesting ImagePickerPluginTestOverrides? overrides, + @visibleForTesting ImageResizer? imageResizer, }) : _overrides = overrides { + _imageResizer = imageResizer ?? ImageResizer(); _target = _ensureInitialized(_kImagePickerInputsDomId); } + final ImagePickerPluginTestOverrides? _overrides; + + bool get _hasOverrides => _overrides != null; + + late html.Element _target; + + late ImageResizer _imageResizer; + /// Registers this class as the default instance of [ImagePickerPlatform]. static void registerWith(Registrar registrar) { ImagePickerPlatform.instance = ImagePickerPlugin(); @@ -55,7 +60,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptImageMimeType, capture: capture); } @@ -77,7 +83,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptVideoMimeType, capture: capture); } @@ -91,7 +98,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { String? accept, String? capture, }) { - html.FileUploadInputElement input = + final html.FileUploadInputElement input = createInputElement(accept, capture) as html.FileUploadInputElement; _injectAndActivate(input); return _getSelectedFile(input); @@ -117,12 +124,18 @@ class ImagePickerPlugin extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); - List files = await getFiles( + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); + final List files = await getFiles( accept: _kAcceptImageMimeType, capture: capture, ); - return files.first; + return _imageResizer.resizeImageIfNeeded( + files.first, + maxWidth, + maxHeight, + imageQuality, + ); } /// Returns an [XFile] containing the video that was picked. @@ -143,8 +156,9 @@ class ImagePickerPlugin extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) async { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); - List files = await getFiles( + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); + final List files = await getFiles( accept: _kAcceptVideoMimeType, capture: capture, ); @@ -157,8 +171,21 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, - }) { - return getFiles(accept: _kAcceptImageMimeType, multiple: true); + }) async { + final List images = await getFiles( + accept: _kAcceptImageMimeType, + multiple: true, + ); + final Iterable> resized = images.map( + (XFile image) => _imageResizer.resizeImageIfNeeded( + image, + maxWidth, + maxHeight, + imageQuality, + ), + ); + + return Future.wait(resized); } /// Injects a file input with the specified accept+capture attributes, and @@ -176,7 +203,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { String? capture, bool multiple = false, }) { - html.FileUploadInputElement input = createInputElement( + final html.FileUploadInputElement input = createInputElement( accept, capture, multiple: multiple, @@ -209,24 +236,24 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Handles the OnChange event from a FileUploadInputElement object /// Returns a list of selected files. List? _handleOnChangeEvent(html.Event event) { - final html.FileUploadInputElement input = - event.target as html.FileUploadInputElement; - return _getFilesFromInput(input); + final html.FileUploadInputElement? input = + event.target as html.FileUploadInputElement?; + return input == null ? null : _getFilesFromInput(input); } /// Monitors an and returns the selected file. Future _getSelectedFile(html.FileUploadInputElement input) { final Completer _completer = Completer(); // Observe the input until we can return something - input.onChange.first.then((event) { - final files = _handleOnChangeEvent(event); + input.onChange.first.then((html.Event event) { + final List? files = _handleOnChangeEvent(event); if (!_completer.isCompleted && files != null) { _completer.complete(PickedFile( html.Url.createObjectUrl(files.first), )); } }); - input.onError.first.then((event) { + input.onError.first.then((html.Event event) { if (!_completer.isCompleted) { _completer.completeError(event); } @@ -241,23 +268,23 @@ class ImagePickerPlugin extends ImagePickerPlatform { Future> _getSelectedXFiles(html.FileUploadInputElement input) { final Completer> _completer = Completer>(); // Observe the input until we can return something - input.onChange.first.then((event) { - final files = _handleOnChangeEvent(event); + input.onChange.first.then((html.Event event) { + final List? files = _handleOnChangeEvent(event); if (!_completer.isCompleted && files != null) { - _completer.complete(files - .map((file) => XFile( - html.Url.createObjectUrl(file), - name: file.name, - length: file.size, - lastModified: DateTime.fromMillisecondsSinceEpoch( - file.lastModified ?? DateTime.now().millisecondsSinceEpoch, - ), - mimeType: file.type, - )) - .toList()); + _completer.complete(files.map((html.File file) { + return XFile( + html.Url.createObjectUrl(file), + name: file.name, + length: file.size, + lastModified: DateTime.fromMillisecondsSinceEpoch( + file.lastModified ?? DateTime.now().millisecondsSinceEpoch, + ), + mimeType: file.type, + ); + }).toList()); } }); - input.onError.first.then((event) { + input.onError.first.then((html.Event event) { if (!_completer.isCompleted) { _completer.completeError(event); } @@ -270,7 +297,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Initializes a DOM container where we can host input elements. html.Element _ensureInitialized(String id) { - var target = html.querySelector('#${id}'); + html.Element? target = html.querySelector('#$id'); if (target == null) { final html.Element targetElement = html.Element.tag('flt-image-picker-inputs')..id = id; @@ -293,7 +320,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { return _overrides!.createInputElement(accept, capture); } - html.Element element = html.FileUploadInputElement() + final html.Element element = html.FileUploadInputElement() ..accept = accept ..multiple = multiple; 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 new file mode 100644 index 000000000000..ba794acae3be --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.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 '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'; + +/// Helper class that resizes images. +class ImageResizer { + /// Resizes the image if needed. + /// (Does not support gif images) + Future resizeImageIfNeeded(XFile file, double? maxWidth, + double? maxHeight, int? imageQuality) async { + if (!imageResizeNeeded(maxWidth, maxHeight, imageQuality) || + file.mimeType == 'image/gif') { + // Implement maxWidth and maxHeight for image/gif + return file; + } + try { + final html.ImageElement imageElement = await loadImage(file.path); + final html.CanvasElement canvas = + resizeImageElement(imageElement, maxWidth, maxHeight); + final XFile resizedImage = + await writeCanvasToFile(file, canvas, imageQuality); + html.Url.revokeObjectUrl(file.path); + return resizedImage; + } catch (e) { + return file; + } + } + + /// function that loads the blobUrl into an imageElement + Future loadImage(String blobUrl) { + final Completer imageLoadCompleter = + Completer(); + final html.ImageElement imageElement = html.ImageElement(); + // ignore: unsafe_html + imageElement.src = blobUrl; + + imageElement.onLoad.listen((html.Event event) { + imageLoadCompleter.complete(imageElement); + }); + imageElement.onError.listen((html.Event event) { + const String exception = 'Error while loading image.'; + imageElement.remove(); + imageLoadCompleter.completeError(exception); + }); + return imageLoadCompleter.future; + } + + /// Draws image to a canvas while resizing the image to fit the [maxWidth],[maxHeight] constraints + html.CanvasElement resizeImageElement( + html.ImageElement source, double? maxWidth, double? maxHeight) { + final Size newImageSize = calculateSizeOfDownScaledImage( + Size(source.width!.toDouble(), source.height!.toDouble()), + maxWidth, + maxHeight); + final html.CanvasElement canvas = html.CanvasElement(); + canvas.width = newImageSize.width.toInt(); + canvas.height = newImageSize.height.toInt(); + final html.CanvasRenderingContext2D context = canvas.context2D; + if (maxHeight == null && maxWidth == null) { + context.drawImage(source, 0, 0); + } else { + context.drawImageScaled(source, 0, 0, canvas.width!, canvas.height!); + } + return canvas; + } + + /// function that converts a canvas element to Xfile + /// [imageQuality] is only supported for jpeg and webp images. + Future writeCanvasToFile( + XFile originalFile, html.CanvasElement canvas, int? imageQuality) async { + final double calculatedImageQuality = + (min(imageQuality ?? 100, 100)) / 100.0; + final html.Blob blob = + await canvas.toBlob(originalFile.mimeType, calculatedImageQuality); + return XFile(html.Url.createObjectUrlFromBlob(blob), + mimeType: originalFile.mimeType, + name: 'scaled_${originalFile.name}', + lastModified: DateTime.now(), + length: blob.size); + } +} diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart new file mode 100644 index 000000000000..e906a88f00fe --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_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. + +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +///a function that checks if an image needs to be resized or not +bool imageResizeNeeded(double? maxWidth, double? maxHeight, int? imageQuality) { + return imageQuality != null + ? isImageQualityValid(imageQuality) + : (maxWidth != null || maxHeight != null); +} + +/// a function that checks if image quality is between 0 to 100 +bool isImageQualityValid(int imageQuality) { + return imageQuality >= 0 && imageQuality <= 100; +} + +/// a function that calculates the size of the downScaled image. +/// imageWidth is the width of the image +/// imageHeight is the height of the image +/// maxWidth is the maximum width of the scaled image +/// maxHeight is the maximum height of the scaled image +Size calculateSizeOfDownScaledImage( + Size imageSize, double? maxWidth, double? maxHeight) { + final double widthFactor = maxWidth != null ? imageSize.width / maxWidth : 1; + final double heightFactor = + maxHeight != null ? imageSize.height / maxHeight : 1; + final double resizeFactor = max(widthFactor, heightFactor); + return resizeFactor > 1 ? imageSize ~/ resizeFactor : imageSize; +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 895486f3de06..508e32aca5bd 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -1,12 +1,12 @@ name: image_picker_for_web description: Web platform implementation of image_picker -repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web +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.3 +version: 2.1.8 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -22,9 +22,7 @@ dependencies: flutter_web_plugins: sdk: flutter image_picker_platform_interface: ^2.2.0 - meta: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart new file mode 100644 index 000000000000..0bfa81729bf0 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.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 'dart:ui'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:image_picker_for_web/src/image_resizer_utils.dart'; + +void main() { + group('Image Resizer Utils', () { + group('calculateSizeOfScaledImage', () { + test( + "scaled image height and width are same if max width and max height are same as image's width and height", + () { + expect(calculateSizeOfDownScaledImage(const Size(500, 300), 500, 300), + const Size(500, 300)); + }); + + test( + 'scaled image height and width are same if max width and max height are null', + () { + expect(calculateSizeOfDownScaledImage(const Size(500, 300), null, null), + const Size(500, 300)); + }); + + test('image size is scaled when maxWidth is set', () { + const Size imageSize = Size(500, 300); + const int maxWidth = 400; + final Size scaledSize = calculateSizeOfDownScaledImage( + Size(imageSize.width, imageSize.height), maxWidth.toDouble(), null); + expect(scaledSize.height <= imageSize.height, true); + expect(scaledSize.width <= maxWidth, true); + }); + + test('image size is scaled when maxHeight is set', () { + const Size imageSize = Size(500, 300); + const int maxHeight = 400; + final Size scaledSize = calculateSizeOfDownScaledImage( + Size(imageSize.width, imageSize.height), + null, + maxHeight.toDouble()); + expect(scaledSize.height <= maxHeight, true); + expect(scaledSize.width <= imageSize.width, true); + }); + + test('image size is scaled when both maxWidth and maxHeight is set', () { + const Size imageSize = Size(1120, 2000); + const int maxHeight = 1200; + const int maxWidth = 99; + final Size scaledSize = calculateSizeOfDownScaledImage( + Size(imageSize.width, imageSize.height), + maxWidth.toDouble(), + maxHeight.toDouble()); + expect(scaledSize.height <= maxHeight, true); + expect(scaledSize.width <= maxWidth, true); + }); + }); + group('imageResizeNeeded', () { + test('image needs to be resized when maxWidth is set', () { + expect(imageResizeNeeded(50, null, null), true); + }); + + test('image needs to be resized when maxHeight is set', () { + expect(imageResizeNeeded(null, 50, null), true); + }); + + test('image needs to be resized when imageQuality is set', () { + expect(imageResizeNeeded(null, null, 100), true); + }); + + test('image will not be resized when imageQuality is not valid', () { + expect(imageResizeNeeded(null, null, 101), false); + expect(imageResizeNeeded(null, null, -1), false); + }); + }); + + group('isImageQualityValid', () { + test('image quality is valid in 0 to 100', () { + expect(isImageQualityValid(50), true); + expect(isImageQualityValid(0), true); + expect(isImageQualityValid(100), true); + }); + + test( + 'image quality is not valid when imageQuality is less than 0 or greater than 100', + () { + expect(isImageQualityValid(-1), false); + expect(isImageQualityValid(101), false); + }); + }); + }); +} diff --git a/packages/connectivity/connectivity_for_web/AUTHORS b/packages/image_picker/image_picker_ios/AUTHORS similarity index 100% rename from packages/connectivity/connectivity_for_web/AUTHORS rename to packages/image_picker/image_picker_ios/AUTHORS 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..ecaaf773ece5 --- /dev/null +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -0,0 +1,36 @@ +## 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..16b5c51839f8 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/README.md @@ -0,0 +1,3 @@ +# image_picker_example + +Demonstrates how to use the `image_picker` plugin. 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/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 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/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/image_picker/image_picker_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 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/device_info/device_info/example/ios/Runner/AppDelegate.h b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/AppDelegate.h rename to packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.h diff --git a/packages/battery/battery/example/ios/Runner/AppDelegate.m b/packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/battery/battery/example/ios/Runner/AppDelegate.m rename to packages/image_picker/image_picker_ios/example/ios/Runner/AppDelegate.m 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/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/device_info/device_info/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 old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/image_picker/image_picker_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png 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/connectivity/connectivity/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard old mode 100644 new mode 100755 similarity index 100% rename from packages/connectivity/connectivity/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard b/packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard old mode 100644 new mode 100755 similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/image_picker/image_picker_ios/example/ios/Runner/Base.lproj/Main.storyboard 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_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m new file mode 100644 index 000000000000..04d491131d5b --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -0,0 +1,266 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "ImagePickerTestImages.h" + +@import image_picker_ios; +@import image_picker_ios.Test; +@import XCTest; +#import + +@interface MockViewController : UIViewController +@property(nonatomic, retain) UIViewController *mockPresented; +@end + +@implementation MockViewController +@synthesize mockPresented; + +- (UIViewController *)presentedViewController { + return mockPresented; +} + +@end + +@interface ImagePickerPluginTests : XCTestCase + +@end + +@implementation ImagePickerPluginTests + +- (void)testPluginPickImageDeviceBack { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceRear is supported + OCMStub(ClassMethod( + [mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + UIImagePickerController *controller = [[UIImagePickerController alloc] init]; + [plugin setImagePickerControllerOverrides:@[ controller ]]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); +} + +- (void)testPluginPickImageDeviceFront { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceFront is supported + OCMStub(ClassMethod( + [mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + UIImagePickerController *controller = [[UIImagePickerController alloc] init]; + [plugin setImagePickerControllerOverrides:@[ controller ]]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); +} + +- (void)testPluginPickVideoDeviceBack { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceRear is supported + OCMStub(ClassMethod( + [mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + UIImagePickerController *controller = [[UIImagePickerController alloc] init]; + [plugin setImagePickerControllerOverrides:@[ controller ]]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); +} + +- (void)testPluginPickVideoDeviceFront { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); + + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceFront is supported + OCMStub(ClassMethod( + [mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + UIImagePickerController *controller = [[UIImagePickerController alloc] init]; + [plugin setImagePickerControllerOverrides:@[ controller ]]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); +} + +- (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { + if (@available(iOS 14, *)) { + return; + } + + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + OCMStub(ClassMethod([photoLibrary authorizationStatus])) + .andReturn(PHAuthorizationStatusAuthorized); + + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + + [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + quality:@(50) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + OCMVerify(times(1), + [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); +} + +#pragma mark - Test camera devices, no op on simulators + +- (void)testPluginPickImageDeviceCancelClickMultipleTimes { + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + return; + } + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + UIImagePickerController *controller = [[UIImagePickerController alloc] init]; + plugin.imagePickerControllerOverrides = @[ controller ]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + // To ensure the flow does not crash by multiple cancel call + [plugin imagePickerControllerDidCancel:controller]; + [plugin imagePickerControllerDidCancel:controller]; +} + +#pragma mark - Test video duration + +- (void)testPickingVideoWithDuration { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + UIImagePickerController *controller = [[UIImagePickerController alloc] init]; + [plugin setImagePickerControllerOverrides:@[ controller ]]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:@(95) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + + XCTAssertEqual(controller.videoMaximumDuration, 95); +} + +- (void)testViewController { + UIWindow *window = [UIWindow new]; + MockViewController *vc1 = [MockViewController new]; + window.rootViewController = vc1; + + UIViewController *vc2 = [UIViewController new]; + vc1.mockPresented = vc2; + + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + XCTAssertEqual([plugin viewControllerWithWindow:window], vc2); +} + +- (void)testPluginMultiImagePathHasNullItem { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block FlutterError *pickImageResult = nil; + 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); + + XCTAssertEqualObjects(pickImageResult.code, @"create_error"); +} + +- (void)testPluginMultiImagePathHasItem { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + NSArray *pathList = @[ @"test" ]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block id pickImageResult = nil; + + 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); + + XCTAssertEqual(pickImageResult, pathList); +} + +@end 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 93% 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 index a0bae7b8f91c..64843f75d05b 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerTestImages.m @@ -6,10 +6,10 @@ @implementation ImagePickerTestImages -+ (NSData*)JPGTestData { - NSBundle* bundle = [NSBundle bundleForClass:self]; - NSURL* url = [bundle URLForResource:@"jpgImage" withExtension:@"jpg"]; - NSData* data = [NSData dataWithContentsOfURL:url]; ++ (NSData *)JPGTestData { + NSBundle *bundle = [NSBundle bundleForClass:self]; + NSURL *url = [bundle URLForResource:@"jpgImage" withExtension:@"jpg"]; + NSData *data = [NSData dataWithContentsOfURL:url]; if (!data.length) { // When the tests are run outside the example project (podspec lint) the image may not be // embedded in the test bundle. Fall back to the base64 string representation of the jpg. @@ -73,10 +73,10 @@ + (NSData*)JPGTestData { return data; } -+ (NSData*)PNGTestData { - NSBundle* bundle = [NSBundle bundleForClass:self]; - NSURL* url = [bundle URLForResource:@"pngImage" withExtension:@"png"]; - NSData* data = [NSData dataWithContentsOfURL:url]; ++ (NSData *)PNGTestData { + NSBundle *bundle = [NSBundle bundleForClass:self]; + NSURL *url = [bundle URLForResource:@"pngImage" withExtension:@"png"]; + NSData *data = [NSData dataWithContentsOfURL:url]; if (!data.length) { // When the tests are run outside the example project (podspec lint) the image may not be // embedded in the test bundle. Fall back to the base64 string representation of the png. @@ -91,10 +91,10 @@ + (NSData*)PNGTestData { return data; } -+ (NSData*)GIFTestData { - NSBundle* bundle = [NSBundle bundleForClass:self]; - NSURL* url = [bundle URLForResource:@"gifImage" withExtension:@"gif"]; - NSData* data = [NSData dataWithContentsOfURL:url]; ++ (NSData *)GIFTestData { + NSBundle *bundle = [NSBundle bundleForClass:self]; + NSURL *url = [bundle URLForResource:@"gifImage" withExtension:@"gif"]; + NSData *data = [NSData dataWithContentsOfURL:url]; if (!data.length) { // When the tests are run outside the example project (podspec lint) the image may not be // embedded in the test bundle. Fall back to the base64 string representation of the gif. 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 b793d6e1f3e0..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,7 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface ImageUtilTests : XCTestCase diff --git a/packages/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/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 54f9469f2053..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,7 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; +@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 98% 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 b81b29f73cef..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,7 +4,8 @@ #import "ImagePickerTestImages.h" -@import image_picker; +@import image_picker_ios; +@import image_picker_ios.Test; @import XCTest; @interface PhotoAssetUtilTests : XCTestCase @@ -101,11 +102,10 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation { size_t numberOfFrames = CGImageSourceGetCount(imageSource); - NSNumber *nilSize = (NSNumber *)[NSNull null]; NSString *savedPathGIF = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataGIF image:imageGIF - maxWidth:nilSize - maxHeight:nilSize + maxWidth:nil + maxHeight:nil imageQuality:nil]; XCTAssertNotNil(savedPathGIF); XCTAssertEqualObjects([savedPathGIF substringFromIndex:savedPathGIF.length - 4], @".gif"); diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m new file mode 100644 index 000000000000..688f5fbee032 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.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 +#import + +@import image_picker_ios; +@import image_picker_ios.Test; +@import XCTest; + +@interface PickerSaveImageToPathOperationTests : XCTestCase + +@end + +@implementation PickerSaveImageToPathOperationTests + +- (void)testSaveWebPImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"webpImage" + withExtension:@"webp"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider + withIdentifier:UTTypeWebP.identifier]; + + [self verifySavingImageWithPickerResult:result]; +} + +- (void)testSavePNGImage API_AVAILABLE(ios(14)) { + 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]; +} + +- (void)testSaveJPGImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImage" + withExtension:@"jpg"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider + withIdentifier:UTTypeWebP.identifier]; + + [self verifySavingImageWithPickerResult:result]; +} + +- (void)testSaveGIFImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"gifImage" + withExtension:@"gif"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider + withIdentifier:UTTypeWebP.identifier]; + + [self verifySavingImageWithPickerResult:result]; +} + +/** + * Creates a mock picker result using NSItemProvider. + * + * @param itemProvider an item provider that will be used as picker result + * @param identifier local identifier of the asset + */ +- (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvider + withIdentifier:(NSString *)identifier API_AVAILABLE(ios(14)) { + PHPickerResult *result = OCMClassMock([PHPickerResult class]); + + OCMStub([result itemProvider]).andReturn(itemProvider); + OCMStub([result assetIdentifier]).andReturn(identifier); + + return result; +} + +/** + * Validates a saving process of FLTPHPickerSaveImageToPathOperation. + * + * FLTPHPickerSaveImageToPathOperation is responsible for saving a picked image to the disk for + * later use. It is expected that the saving is always successful. + * + * @param result the picker result + */ +- (void)verifySavingImageWithPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) { + XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; + + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:@100 + maxWidth:@100 + desiredImageQuality:@100 + savedPathBlock:^(NSString *savedPath) { + if ([[NSFileManager defaultManager] fileExistsAtPath:savedPath]) { + [pathExpectation fulfill]; + } + }]; + + [operation start]; + [self waitForExpectations:@[ pathExpectation ] timeout:30]; +} + +@end 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 89% 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 index 4b2163d00577..e081cee9cce4 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -9,7 +9,7 @@ @interface ImagePickerFromGalleryUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @@ -24,9 +24,9 @@ - (void)setUp { [self.app launch]; __weak typeof(self) weakSelf = self; [self addUIInterruptionMonitorWithDescription:@"Permission popups" - handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { + handler:^BOOL(XCUIElement *_Nonnull interruptingElement) { if (@available(iOS 14, *)) { - XCUIElement* allPhotoPermission = + XCUIElement *allPhotoPermission = interruptingElement .buttons[@"Allow Access to All Photos"]; if (![allPhotoPermission waitForExistenceWithTimeout: @@ -39,7 +39,7 @@ - (void)setUp { } [allPhotoPermission tap]; } else { - XCUIElement* ok = interruptingElement.buttons[@"OK"]; + XCUIElement *ok = interruptingElement.buttons[@"OK"]; if (![ok waitForExistenceWithTimeout: kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", @@ -69,10 +69,10 @@ - (void)testCancel { - (void)launchPickerAndCancel { // Find and tap on the pick from gallery button. - NSPredicate* predicateToFindImageFromGalleryButton = + NSPredicate *predicateToFindImageFromGalleryButton = [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; - XCUIElement* imageFromGalleryButton = + XCUIElement *imageFromGalleryButton = [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; if (![imageFromGalleryButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); @@ -84,10 +84,10 @@ - (void)launchPickerAndCancel { [imageFromGalleryButton tap]; // Find and tap on the `pick` button. - NSPredicate* predicateToFindPickButton = + NSPredicate *predicateToFindPickButton = [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; - XCUIElement* pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + XCUIElement *pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; if (![pickButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pick button with %@ seconds", @(kElementWaitingTime)); @@ -101,10 +101,10 @@ - (void)launchPickerAndCancel { [self.app tap]; // Find and tap on the `Cancel` button. - NSPredicate* predicateToFindCancelButton = + NSPredicate *predicateToFindCancelButton = [NSPredicate predicateWithFormat:@"label == %@", @"Cancel"]; - XCUIElement* cancelButton = + XCUIElement *cancelButton = [self.app.buttons elementMatchingPredicate:predicateToFindCancelButton]; if (![cancelButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); @@ -116,7 +116,7 @@ - (void)launchPickerAndCancel { [cancelButton tap]; // Find the "not picked image text". - XCUIElement* imageNotPickedText = [self.app.staticTexts + XCUIElement *imageNotPickedText = [self.app.staticTexts elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"You have not yet picked an image."]]; @@ -131,10 +131,10 @@ - (void)launchPickerAndCancel { - (void)launchPickerAndPick { // Find and tap on the pick from gallery button. - NSPredicate* predicateToFindImageFromGalleryButton = + NSPredicate *predicateToFindImageFromGalleryButton = [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; - XCUIElement* imageFromGalleryButton = + XCUIElement *imageFromGalleryButton = [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; if (![imageFromGalleryButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); @@ -146,10 +146,10 @@ - (void)launchPickerAndPick { [imageFromGalleryButton tap]; // Find and tap on the `pick` button. - NSPredicate* predicateToFindPickButton = + NSPredicate *predicateToFindPickButton = [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; - XCUIElement* pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + XCUIElement *pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; if (![pickButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pick button with %@ seconds", @(kElementWaitingTime)); @@ -163,11 +163,11 @@ - (void)launchPickerAndPick { [self.app tap]; // Find an image and tap on it. (IOS 14 UI, images are showing directly) - XCUIElement* aImage; + XCUIElement *aImage; if (@available(iOS 14, *)) { aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; } else { - XCUIElement* allPhotosCell = [self.app.cells + XCUIElement *allPhotosCell = [self.app.cells elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"All Photos"]]; if (![allPhotosCell waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); @@ -188,10 +188,10 @@ - (void)launchPickerAndPick { [aImage tap]; // Find the picked image. - NSPredicate* predicateToFindPickedImage = + NSPredicate *predicateToFindPickedImage = [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; - XCUIElement* pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; + XCUIElement *pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; if (![pickedImage waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pickedImage with %@ seconds", @(kElementWaitingTime)); 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 90% 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 index 802a494b0f5e..455fd6269d4b 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m @@ -9,7 +9,7 @@ @interface ImagePickerFromLimitedGalleryUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @@ -24,8 +24,8 @@ - (void)setUp { [self.app launch]; __weak typeof(self) weakSelf = self; [self addUIInterruptionMonitorWithDescription:@"Permission popups" - handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { - XCUIElement* limitedPhotoPermission = + handler:^BOOL(XCUIElement *_Nonnull interruptingElement) { + XCUIElement *limitedPhotoPermission = [interruptingElement.buttons elementBoundByIndex:0]; if (![limitedPhotoPermission waitForExistenceWithTimeout: @@ -57,10 +57,10 @@ - (void)testSelectingFromGallery { - (void)launchPickerAndSelect { // Find and tap on the pick from gallery button. - NSPredicate* predicateToFindImageFromGalleryButton = + NSPredicate *predicateToFindImageFromGalleryButton = [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; - XCUIElement* imageFromGalleryButton = + XCUIElement *imageFromGalleryButton = [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; if (![imageFromGalleryButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); @@ -72,10 +72,10 @@ - (void)launchPickerAndSelect { [imageFromGalleryButton tap]; // Find and tap on the `pick` button. - NSPredicate* predicateToFindPickButton = + NSPredicate *predicateToFindPickButton = [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; - XCUIElement* pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + XCUIElement *pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; if (![pickButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTSkip(@"Pick button isn't found so the test is skipped..."); @@ -89,11 +89,11 @@ - (void)launchPickerAndSelect { [self.app tap]; // Find an image and tap on it. (IOS 14 UI, images are showing directly) - XCUIElement* aImage; + XCUIElement *aImage; if (@available(iOS 14, *)) { aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; } else { - XCUIElement* selectedPhotosCell = [self.app.cells + XCUIElement *selectedPhotosCell = [self.app.cells elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"Selected Photos"]]; if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { @@ -116,10 +116,10 @@ - (void)launchPickerAndSelect { [aImage tap]; // Find and tap on the `Done` button. - NSPredicate* predicateToFindDoneButton = + NSPredicate *predicateToFindDoneButton = [NSPredicate predicateWithFormat:@"label == %@", @"Done"]; - XCUIElement* doneButton = [self.app.buttons elementMatchingPredicate:predicateToFindDoneButton]; + XCUIElement *doneButton = [self.app.buttons elementMatchingPredicate:predicateToFindDoneButton]; if (![doneButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTSkip(@"Permissions popup could not fired so the test is skipped..."); @@ -132,7 +132,7 @@ - (void)launchPickerAndSelect { if (@available(iOS 14, *)) { aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; } else { - XCUIElement* selectedPhotosCell = [self.app.cells + XCUIElement *selectedPhotosCell = [self.app.cells elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"Selected Photos"]]; if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { @@ -155,10 +155,10 @@ - (void)launchPickerAndSelect { [aImage tap]; // Find the picked image. - NSPredicate* predicateToFindPickedImage = + NSPredicate *predicateToFindPickedImage = [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; - XCUIElement* pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; + XCUIElement *pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; if (![pickedImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pickedImage with %@ seconds", diff --git a/packages/share/example/ios/RunnerTests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/share/example/ios/RunnerTests/Info.plist rename to packages/image_picker/image_picker_ios/example/ios/RunnerUITests/Info.plist 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/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Info.plist b/packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Info.plist rename to packages/image_picker/image_picker_ios/example/ios/image_picker_exampleTests/Info.plist 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..c5372b8e7ad8 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/lib/main.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: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(); + } + } + + @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..84fa77e64d70 --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +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.8.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.3.0 + video_player: ^2.1.4 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/local_auth/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart similarity index 100% rename from packages/local_auth/example/test_driver/integration_test.dart rename to packages/image_picker/image_picker_ios/example/test_driver/integration_test.dart diff --git a/packages/camera/camera/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/camera/camera/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 81% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.h index b0edd03e5076..5e77a6ca67ae 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.h @@ -18,9 +18,10 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTImagePickerImageUtil : NSObject +// Resizes the given image to fit within maxWidth (if non-nil) and maxHeight (if non-nil) + (UIImage *)scaledImage:(UIImage *)image - maxWidth:(NSNumber *)maxWidth - maxHeight:(NSNumber *)maxHeight + maxWidth:(nullable NSNumber *)maxWidth + maxHeight:(nullable NSNumber *)maxHeight isMetadataAvailable:(BOOL)isMetadataAvailable; // Resize all gif animation frames. 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 98% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m index 7b454072ecff..2d370aa2e6c8 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m @@ -35,8 +35,8 @@ + (UIImage *)scaledImage:(UIImage *)image double originalWidth = image.size.width; double originalHeight = image.size.height; - bool hasMaxWidth = maxWidth != (id)[NSNull null]; - bool hasMaxHeight = maxHeight != (id)[NSNull null]; + bool hasMaxWidth = maxWidth != nil; + bool hasMaxHeight = maxHeight != nil; double width = hasMaxWidth ? MIN([maxWidth doubleValue], originalWidth) : originalWidth; double height = hasMaxHeight ? MIN([maxHeight doubleValue], originalHeight) : originalHeight; 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 87% rename from packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h index ffd23cd3df6a..c88db0bad72f 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h @@ -8,7 +8,6 @@ @interface FLTImagePickerPlugin : NSObject // For testing only. -- (UIImagePickerController *)getImagePickerController; - (UIViewController *)viewControllerWithWindow:(UIWindow *)window; @end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m new file mode 100644 index 000000000000..18d4ad2f054c --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -0,0 +1,665 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTImagePickerPlugin.h" +#import "FLTImagePickerPlugin_Test.h" + +#import +#import +#import +#import +#import +#import + +#import "FLTImagePickerImageUtil.h" +#import "FLTImagePickerMetaDataUtil.h" +#import "FLTImagePickerPhotoAssetUtil.h" +#import "FLTPHPickerSaveImageToPathOperation.h" +#import "messages.g.h" + +@implementation FLTImagePickerMethodCallContext +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { + if (self = [super init]) { + _result = [result copy]; + } + return self; +} +@end + +#pragma mark - + +@interface FLTImagePickerPlugin () + +/** + * The PHPickerViewController instance used to pick multiple + * images. + */ +@property(strong, nonatomic) PHPickerViewController *pickerViewController API_AVAILABLE(ios(14)); + +/** + * The 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. + */ +@property(strong, nonatomic) + NSMutableArray *imagePickerControllerOverrides; + +@end + +typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPickerClassType }; + +@implementation FLTImagePickerPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new]; + FLTImagePickerApiSetup(registrar.messenger, instance); +} + +- (UIImagePickerController *)createImagePickerController { + if ([self.imagePickerControllerOverrides count] > 0) { + UIImagePickerController *controller = [self.imagePickerControllerOverrides firstObject]; + [self.imagePickerControllerOverrides removeObjectAtIndex:0]; + return controller; + } + + return [[UIImagePickerController alloc] init]; +} + +- (void)setImagePickerControllerOverrides: + (NSArray *)imagePickerControllers { + _imagePickerControllerOverrides = [imagePickerControllers mutableCopy]; +} + +- (UIViewController *)viewControllerWithWindow:(UIWindow *)window { + UIWindow *windowToUse = window; + if (windowToUse == nil) { + for (UIWindow *window in [UIApplication sharedApplication].windows) { + if (window.isKeyWindow) { + windowToUse = window; + break; + } + } + } + + UIViewController *topController = windowToUse.rootViewController; + while (topController.presentedViewController) { + topController = topController.presentedViewController; + } + return topController; +} + +/** + * Returns the UIImagePickerControllerCameraDevice to use given [source]. + * + * @param source The source specification from Dart. + */ +- (UIImagePickerControllerCameraDevice)cameraDeviceForSource:(FLTSourceSpecification *)source { + switch (source.camera) { + case FLTSourceCameraFront: + return UIImagePickerControllerCameraDeviceFront; + case FLTSourceCameraRear: + return UIImagePickerControllerCameraDeviceRear; + } +} + +- (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)context + API_AVAILABLE(ios(14)) { + PHPickerConfiguration *config = + [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; + config.selectionLimit = context.maxImageCount; + config.filter = [PHPickerFilter imagesFilter]; + + _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; + _pickerViewController.delegate = self; + _pickerViewController.presentationController.delegate = self; + self.callContext = context; + + [self checkPhotoAuthorizationForAccessLevel]; +} + +- (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; + + 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 image source." + details:nil]]; + break; + } +} + +#pragma mark - FLTImagePickerApi + +- (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source + maxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + 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; + + if (source.type == FLTSourceTypeGallery) { // Capture is not possible with PHPicker + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + [self launchUIImagePickerWithSource:source context:context]; + } + } else { + [self launchUIImagePickerWithSource:source context:context]; + } +} + +- (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; + } +} + +- (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source + maxDuration:(nullable NSNumber *)maxDurationSeconds + completion: + (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; + } + } + // Camera is not available on simulators + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && + [UIImagePickerController isCameraDeviceAvailable:device]) { + imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; + imagePickerController.cameraDevice = device; + [[self viewControllerWithWindow:nil] presentViewController:imagePickerController + animated:YES + completion:nil]; + } else { + UIAlertController *cameraErrorAlert = [UIAlertController + alertControllerWithTitle:NSLocalizedString(@"Error", @"Alert title when camera unavailable") + message:NSLocalizedString(@"Camera not available.", + "Alert message when camera unavailable") + preferredStyle:UIAlertControllerStyleAlert]; + [cameraErrorAlert + addAction:[UIAlertAction actionWithTitle:NSLocalizedString( + @"OK", @"Alert button when camera unavailable") + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action){ + }]]; + [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert + animated:YES + completion:nil]; + [self sendCallResultWithSavedPathList:nil]; + } +} + +- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController + camera:(UIImagePickerControllerCameraDevice)device { + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + + switch (status) { + case AVAuthorizationStatusAuthorized: + [self showCamera:device withImagePicker:imagePickerController]; + break; + case AVAuthorizationStatusNotDetermined: { + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo + completionHandler:^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (granted) { + [self showCamera:device withImagePicker:imagePickerController]; + } else { + [self errorNoCameraAccess:AVAuthorizationStatusDenied]; + } + }); + }]; + break; + } + case AVAuthorizationStatusDenied: + case AVAuthorizationStatusRestricted: + default: + [self errorNoCameraAccess:status]; + break; + } +} + +- (void)checkPhotoAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController { + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + switch (status) { + case PHAuthorizationStatusNotDetermined: { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + [self showPhotoLibraryWithImagePicker:imagePickerController]; + } else { + [self errorNoPhotoAccess:status]; + } + }); + }]; + break; + } + case PHAuthorizationStatusAuthorized: + [self showPhotoLibraryWithImagePicker:imagePickerController]; + break; + case PHAuthorizationStatusDenied: + case PHAuthorizationStatusRestricted: + default: + [self errorNoPhotoAccess:status]; + break; + } +} + +- (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + switch (status) { + case PHAuthorizationStatusNotDetermined: { + [PHPhotoLibrary + requestAuthorizationForAccessLevel:PHAccessLevelReadWrite + handler:^(PHAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + [self + showPhotoLibraryWithPHPicker:self-> + _pickerViewController]; + } else if (status == PHAuthorizationStatusLimited) { + [self + showPhotoLibraryWithPHPicker:self-> + _pickerViewController]; + } else { + [self errorNoPhotoAccess:status]; + } + }); + }]; + break; + } + case PHAuthorizationStatusAuthorized: + case PHAuthorizationStatusLimited: + [self showPhotoLibraryWithPHPicker:_pickerViewController]; + break; + case PHAuthorizationStatusDenied: + case PHAuthorizationStatusRestricted: + default: + [self errorNoPhotoAccess:status]; + break; + } +} + +- (void)errorNoCameraAccess:(AVAuthorizationStatus)status { + switch (status) { + case AVAuthorizationStatusRestricted: + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_restricted" + message:@"The user is not allowed to use the camera." + details:nil]]; + break; + case AVAuthorizationStatusDenied: + default: + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_denied" + message:@"The user did not allow camera access." + details:nil]]; + break; + } +} + +- (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { + switch (status) { + case PHAuthorizationStatusRestricted: + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_restricted" + message:@"The user is not allowed to use the photo." + details:nil]]; + break; + case PHAuthorizationStatusDenied: + default: + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_denied" + message:@"The user did not allow photo access." + details:nil]]; + break; + } +} + +- (void)showPhotoLibraryWithPHPicker:(PHPickerViewController *)pickerViewController + API_AVAILABLE(ios(14)) { + [[self viewControllerWithWindow:nil] presentViewController:pickerViewController + animated:YES + completion:nil]; +} + +- (void)showPhotoLibraryWithImagePicker:(UIImagePickerController *)imagePickerController { + imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + [[self viewControllerWithWindow:nil] presentViewController:imagePickerController + animated:YES + completion:nil]; +} + +- (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { + if (![imageQuality isKindOfClass:[NSNumber class]]) { + imageQuality = @1; + } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) { + imageQuality = @1; + } else { + imageQuality = @([imageQuality floatValue] / 100); + } + return imageQuality; +} + +#pragma mark - UIAdaptivePresentationControllerDelegate + +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { + [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) { + [self sendCallResultWithSavedPathList:nil]; + return; + } + dispatch_queue_t backgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + dispatch_async(backgroundQueue, ^{ + 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; + }]; + [operationQueue addOperation:operation]; + } + [operationQueue waitUntilAllOperationsAreFinished]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self sendCallResultWithSavedPathList:pathList]; + }); + }); +} + +#pragma mark - + +/** + * Creates an NSMutableArray of a certain size filled with NSNull objects. + * + * The difference with initWithCapacity is that initWithCapacity still gives an empty array making + * it impossible to add objects on an index larger than the size. + * + * @param size The length of the required array + * @return NSMutableArray An array of a specified size + */ +- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { + NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size]; + for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++) + ; + return mutableArray; +} + +#pragma mark - UIImagePickerControllerDelegate + +- (void)imagePickerController:(UIImagePickerController *)picker + didFinishPickingMediaWithInfo:(NSDictionary *)info { + NSURL *videoURL = info[UIImagePickerControllerMediaURL]; + [picker dismissViewControllerAnimated:YES completion:nil]; + // The method dismissViewControllerAnimated does not immediately prevent + // further didFinishPickingMediaWithInfo invocations. A nil check is necessary + // to prevent below code to be unwantly executed multiple times and cause a + // crash. + if (!self.callContext) { + return; + } + if (videoURL != nil) { + if (@available(iOS 13.0, *)) { + NSString *fileName = [videoURL lastPathComponent]; + NSURL *destination = + [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; + + if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { + NSError *error; + if (![[videoURL path] isEqualToString:[destination path]]) { + [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; + + if (error) { + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; + return; + } + } + videoURL = destination; + } + } + [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; + } else { + UIImage *image = info[UIImagePickerControllerEditedImage]; + if (image == nil) { + image = info[UIImagePickerControllerOriginalImage]; + } + 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]; + + if (maxWidth != nil || maxHeight != nil) { + image = [FLTImagePickerImageUtil scaledImage:image + maxWidth:maxWidth + maxHeight:maxHeight + isMetadataAvailable:YES]; + } + + if (!originalAsset) { + // Image picked without an original asset (e.g. User took a photo directly) + [self saveImageWithPickerInfo:info image:image imageQuality:desiredImageQuality]; + } else { + 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]; + [self sendCallResultWithSavedPathList:nil]; +} + +#pragma mark - + +- (void)saveImageWithOriginalImageData:(NSData *)originalImageData + image:(UIImage *)image + maxWidth:(NSNumber *)maxWidth + maxHeight:(NSNumber *)maxHeight + imageQuality:(NSNumber *)imageQuality { + NSString *savedPath = + [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:originalImageData + image:image + maxWidth:maxWidth + maxHeight:maxHeight + imageQuality:imageQuality]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; +} + +- (void)saveImageWithPickerInfo:(NSDictionary *)info + image:(UIImage *)image + imageQuality:(NSNumber *)imageQuality { + NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info + image:image + imageQuality:imageQuality]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; +} + +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList { + if (!self.callContext) { + return; + } + + if ([pathList containsObject:[NSNull null]]) { + self.callContext.result(nil, [FlutterError errorWithCode:@"create_error" + message:@"pathList's items should not be null" + details:nil]); + } else { + 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.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..64c20452987d --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h @@ -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. + +// 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; + +@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 100% rename from packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h rename to packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m new file mode 100644 index 000000000000..a81c95f1b120 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors. 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 "FLTPHPickerSaveImageToPathOperation.h" + +API_AVAILABLE(ios(14)) +@interface FLTPHPickerSaveImageToPathOperation () + +@property(strong, nonatomic) PHPickerResult *result; +@property(assign, nonatomic) NSNumber *maxHeight; +@property(assign, nonatomic) NSNumber *maxWidth; +@property(assign, nonatomic) NSNumber *desiredImageQuality; + +@end + +typedef void (^GetSavedPath)(NSString *); + +@implementation FLTPHPickerSaveImageToPathOperation { + BOOL executing; + BOOL finished; + GetSavedPath getSavedPath; +} + +- (instancetype)initWithResult:(PHPickerResult *)result + maxHeight:(NSNumber *)maxHeight + maxWidth:(NSNumber *)maxWidth + desiredImageQuality:(NSNumber *)desiredImageQuality + savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { + if (self = [super init]) { + if (result) { + self.result = result; + self.maxHeight = maxHeight; + self.maxWidth = maxWidth; + self.desiredImageQuality = desiredImageQuality; + getSavedPath = savedPathBlock; + executing = NO; + finished = NO; + } else { + return nil; + } + return self; + } else { + return nil; + } +} + +- (BOOL)isConcurrent { + return YES; +} + +- (BOOL)isExecuting { + return executing; +} + +- (BOOL)isFinished { + return finished; +} + +- (void)setFinished:(BOOL)isFinished { + [self willChangeValueForKey:@"isFinished"]; + self->finished = isFinished; + [self didChangeValueForKey:@"isFinished"]; +} + +- (void)setExecuting:(BOOL)isExecuting { + [self willChangeValueForKey:@"isExecuting"]; + self->executing = isExecuting; + [self didChangeValueForKey:@"isExecuting"]; +} + +- (void)completeOperationWithPath:(NSString *)savedPath { + [self setExecuting:NO]; + [self setFinished:YES]; + getSavedPath(savedPath); +} + +- (void)start { + if ([self isCancelled]) { + [self setFinished:YES]; + return; + } + if (@available(iOS 14, *)) { + [self setExecuting:YES]; + + if ([self.result.itemProvider hasItemConformingToTypeIdentifier:UTTypeWebP.identifier]) { + [self.result.itemProvider + loadDataRepresentationForTypeIdentifier:UTTypeWebP.identifier + completionHandler:^(NSData *_Nullable data, + NSError *_Nullable error) { + UIImage *image = [[UIImage alloc] initWithData:data]; + [self processImage:image]; + }]; + return; + } + + [self.result.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + if ([image isKindOfClass:[UIImage class]]) { + [self processImage:image]; + } + }]; + } else { + [self setFinished:YES]; + } +} + +/** + * Processes the image. + */ +- (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { + PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result]; + + if (self.maxWidth != nil || self.maxHeight != nil) { + localImage = [FLTImagePickerImageUtil scaledImage:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + isMetadataAvailable:originalAsset != nil]; + } + if (originalAsset) { + 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 = + [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil + image:localImage + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath]; + } +} + +@end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap b/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap new file mode 100644 index 000000000000..0d60b684a256 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/ImagePickerPlugin.modulemap @@ -0,0 +1,14 @@ +framework module image_picker_ios { + umbrella header "image_picker_ios-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FLTImagePickerPlugin_Test.h" + header "FLTImagePickerImageUtil.h" + header "FLTImagePickerMetaDataUtil.h" + header "FLTImagePickerPhotoAssetUtil.h" + header "FLTPHPickerSaveImageToPathOperation.h" + } +} diff --git a/packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h b/packages/image_picker/image_picker_ios/ios/Classes/image_picker_ios-umbrella.h new file mode 100644 index 000000000000..0e23d6d9d60a --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/image_picker_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/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..310165f72f4f --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.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. +// Autogenerated from Pigeon (v3.0.2), 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 + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + 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..6c91c0ab264f --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.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. +// Autogenerated from Pigeon (v3.0.2), 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:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickImageWithSource:maxSize:quality: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); + [api pickImageWithSource:arg_source + maxSize:arg_maxSize + quality:arg_imageQuality + 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:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMultiImageWithMaxSize:quality:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1); + [api pickMultiImageWithMaxSize:arg_maxSize + quality:arg_imageQuality + 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/ios/image_picker_ios.podspec b/packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec new file mode 100644 index 000000000000..549c5f09e1f8 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/image_picker_ios.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'image_picker_ios' + s.version = '0.0.1' + s.summary = 'Flutter plugin that shows an image picker.' + s.description = <<-DESC +A Flutter plugin for picking images from the image library, and taking new pictures with the camera. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/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' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end 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..3d1413cf0cce --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -0,0 +1,209 @@ +// Copyright 2013 The Flutter Authors. 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, + 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 _pickMultiImageAsPath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => PickedFile(path as String)).toList(); + } + + Future?> _pickMultiImageAsPath({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + 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'); + } + + // 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)) + ?.cast(); + } + + Future _pickImageAsPath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + 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 _hostApi.pickImage( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(preferredCameraDevice)), + MaxSize(width: maxWidth, height: maxHeight), + imageQuality, + ); + } + + @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, + 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( + 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..0c5859e80ac9 --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -0,0 +1,194 @@ +// Copyright 2013 The Flutter 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.2), 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) 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]) + 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) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_maxSize, arg_imageQuality]) + 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..94ac034606e9 --- /dev/null +++ b/packages/image_picker/image_picker_ios/pigeons/messages.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. + +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:') + String? pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality); + @async + @ObjCSelector('pickMultiImageWithMaxSize:quality:') + List? pickMultiImage(MaxSize maxSize, int? imageQuality); + @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..314a52e94510 --- /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.5+6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: image_picker + platforms: + ios: + dartPluginClass: ImagePickerIOS + pluginClass: FLTImagePickerPlugin + +dependencies: + flutter: + sdk: flutter + image_picker_platform_interface: ^2.3.0 + +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..09517f1ef96b --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -0,0 +1,937 @@ +// Copyright 2013 The Flutter Authors. 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) 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, + })); + return returnValue as String?; + } + + @override + Future?> pickMultiImage( + MaxSize maxSize, int? imageQuality) async { + calls.add(_LoggedMethodCall('pickMultiImage', arguments: { + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + })); + 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 + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + 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 + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + 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, + }), + ], + ); + }); + + 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, + }), + ], + ); + }); + }); + + 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, + }), + ], + ); + }); + + 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, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + 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 + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + 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 + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': SourceCamera.rear + }), + ], + ); + }); + + 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, + }), + ], + ); + }); + + 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, + }), + ], + ); + }); + }); + + 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, + }), + ], + ); + }); + + 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, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + 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, + }), + ], + ); + }); + }); +} 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..8b20f2129d89 --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/test_api.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. +// Autogenerated from Pigeon (v3.0.2), 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); + Future?> pickMultiImage(MaxSize maxSize, int? imageQuality); + 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 String? output = + await api.pickImage(arg_source!, arg_maxSize!, arg_imageQuality); + 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 List? output = + await api.pickMultiImage(arg_maxSize!, arg_imageQuality); + 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 d637ac1a277e..0a4e98bf7dbe 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,23 @@ +## 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. + +## 2.4.3 + +* Removes dependency on `meta`. + +## 2.4.2 + +* Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. + ## 2.4.1 * Reverts the changes from 2.4.0, which was a breaking change that diff --git a/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart b/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart index 133c05ecfebf..bdc168617567 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart @@ -2,6 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'package:cross_file/cross_file.dart'; export 'package:image_picker_platform_interface/src/platform_interface/image_picker_platform.dart'; export 'package:image_picker_platform_interface/src/types/types.dart'; -export 'package:cross_file/cross_file.dart'; 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 b02284e957fa..ba5d60d7a677 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 @@ -6,11 +6,10 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show visibleForTesting; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); +const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); /// An implementation of [ImagePickerPlatform] that uses method channels. class MethodChannelImagePicker extends ImagePickerPlatform { @@ -26,7 +25,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String? path = await _getImagePath( + final String? path = await _getImagePath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -47,9 +46,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxHeight: maxHeight, imageQuality: imageQuality, ); - if (paths == null) return null; + if (paths == null) { + return null; + } - return paths.map((path) => PickedFile(path)).toList(); + return paths.map((dynamic path) => PickedFile(path as String)).toList(); } Future?> _getMultiImagePath({ @@ -86,6 +87,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( @@ -107,7 +109,8 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, - 'cameraDevice': preferredCameraDevice.index + 'cameraDevice': preferredCameraDevice.index, + 'requestFullMetadata': requestFullMetadata, }, ); } @@ -152,7 +155,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); - final String? type = result['type']; + final String? type = result['type'] as String?; assert(type == kTypeImage || type == kTypeVideo); RetrieveType? retrieveType; @@ -165,10 +168,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { PlatformException? exception; if (result.containsKey('errorCode')) { exception = PlatformException( - code: result['errorCode'], message: result['errorMessage']); + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); } - final String? path = result['path']; + final String? path = result['path'] as String?; return LostData( file: path != null ? PickedFile(path) : null, @@ -185,7 +189,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String? path = await _getImagePath( + final String? path = await _getImagePath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -195,6 +199,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, @@ -206,9 +226,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxHeight: maxHeight, imageQuality: imageQuality, ); - if (paths == null) return null; + if (paths == null) { + return null; + } - return paths.map((path) => XFile(path)).toList(); + return paths.map((dynamic path) => XFile(path as String)).toList(); } @override @@ -229,7 +251,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { Future getLostData() async { List? pickedFileList; - Map? result = + final Map? result = await _channel.invokeMapMethod('retrieve'); if (result == null) { @@ -238,7 +260,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); - final String? type = result['type']; + final String? type = result['type'] as String?; assert(type == kTypeImage || type == kTypeVideo); RetrieveType? retrieveType; @@ -251,15 +273,17 @@ class MethodChannelImagePicker extends ImagePickerPlatform { PlatformException? exception; if (result.containsKey('errorCode')) { exception = PlatformException( - code: result['errorCode'], message: result['errorMessage']); + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); } - final String? path = result['path']; + final String? path = result['path'] as String?; - final pathList = result['pathList']; + final List? pathList = + (result['pathList'] as List?)?.cast(); if (pathList != null) { - pickedFileList = []; - for (String path in pathList) { + pickedFileList = []; + for (final String path in pathList) { pickedFileList.add(XFile(path)); } } 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 5c1c8b698442..d1d06f904fe6 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,9 +5,9 @@ import 'dart:async'; import 'package:cross_file/cross_file.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.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'; /// The interface that implementations of image_picker must implement. /// @@ -34,7 +34,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { // TODO(amirh): Extract common platform interface logic. // https://github.com/flutter/flutter/issues/43368 static set instance(ImagePickerPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } @@ -146,6 +146,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 @@ -251,4 +253,34 @@ 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, + ); + } } 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..cdc89a920178 --- /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 'package:image_picker_platform_interface/src/types/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/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart index de259e0611dd..77bf87ca045d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart @@ -5,7 +5,7 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart' show immutable; /// The interface for a PickedFile. /// @@ -18,7 +18,8 @@ import 'package:meta/meta.dart'; @immutable abstract class PickedFileBase { /// Construct a PickedFile - PickedFileBase(String path); + // ignore: avoid_unused_constructor_parameters + const PickedFileBase(String path); /// Get the path of the picked file. /// diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart index 24e1931008b6..7d9761a57602 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart @@ -13,20 +13,21 @@ import './base.dart'; /// /// It wraps the bytes of a selected file. class PickedFile extends PickedFileBase { - final String path; - final Uint8List? _initBytes; - /// Construct a PickedFile object from its ObjectUrl. /// /// Optionally, this can be initialized with `bytes` /// so no http requests are performed to retrieve files later. - PickedFile(this.path, {Uint8List? bytes}) + const PickedFile(this.path, {Uint8List? bytes}) : _initBytes = bytes, super(path); + @override + final String path; + final Uint8List? _initBytes; + Future get _bytes async { if (_initBytes != null) { - return Future.value(UnmodifiableUint8ListView(_initBytes!)); + return Future.value(UnmodifiableUint8ListView(_initBytes!)); } return http.readBytes(Uri.parse(path)); } @@ -38,12 +39,12 @@ class PickedFile extends PickedFileBase { @override Future readAsBytes() async { - return Future.value(await _bytes); + return Future.value(await _bytes); } @override Stream openRead([int? start, int? end]) async* { - final bytes = await _bytes; + final Uint8List bytes = await _bytes; yield bytes.sublist(start ?? 0, end ?? bytes.length); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index 7037b6b7121a..500cc65a0870 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -10,13 +10,13 @@ import './base.dart'; /// A PickedFile backed by a dart:io File. class PickedFile extends PickedFileBase { - final File _file; - /// Construct a PickedFile object backed by a dart:io File. PickedFile(String path) : _file = File(path), super(path); + final File _file; + @override String get path { return _file.path; @@ -36,6 +36,6 @@ class PickedFile extends PickedFileBase { Stream openRead([int? start, int? end]) { return _file .openRead(start ?? 0, end) - .map((chunk) => Uint8List.fromList(chunk)); + .map((List chunk) => Uint8List.fromList(chunk)); } } 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 ad7cd3fbcaab..dad86c5d1ba1 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,10 +3,11 @@ // found in the LICENSE file. export 'camera_device.dart'; +export 'image_picker_options.dart'; export 'image_source.dart'; -export 'retrieve_type.dart'; -export 'picked_file/picked_file.dart'; export 'lost_data_response.dart'; +export 'picked_file/picked_file.dart'; +export 'retrieve_type.dart'; /// Denotes that an image is being picked. const String kTypeImage = 'image'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index e41137fcb06b..4ce1d2fc52f1 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -1,24 +1,22 @@ name: image_picker_platform_interface description: A common platform interface for the image_picker plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_platform_interface 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.1 +version: 2.5.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: + cross_file: ^0.3.1+1 flutter: sdk: flutter http: ^0.13.0 - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 - cross_file: ^0.3.1+1 + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 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 17caa8456621..72ed363ef7ae 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 @@ -12,7 +12,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$MethodChannelImagePicker', () { - MethodChannelImagePicker picker = MethodChannelImagePicker(); + final MethodChannelImagePicker picker = MethodChannelImagePicker(); final List log = []; dynamic returnValue = ''; @@ -40,14 +40,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 +95,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 +205,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -215,6 +225,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'requestFullMetadata': true, }), ], ); @@ -223,7 +234,7 @@ void main() { group('#pickMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.pickMultiImage(); expect( @@ -239,7 +250,7 @@ void main() { }); test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.pickMultiImage(); await picker.pickMultiImage( maxWidth: 10.0, @@ -308,7 +319,7 @@ void main() { }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; + returnValue = ['0', '1']; expect( () => picker.pickMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -320,8 +331,8 @@ void main() { ); }); - test('does not accept a invalid imageQuality argument', () { - returnValue = ['0', '1']; + test('does not accept an invalid imageQuality argument', () { + returnValue = ['0', '1']; expect( () => picker.pickMultiImage(imageQuality: -1), throwsArgumentError, @@ -509,14 +520,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 +575,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 +684,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'requestFullMetadata': true, }), ], ); @@ -683,6 +704,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'requestFullMetadata': true, }), ], ); @@ -691,7 +713,7 @@ void main() { group('#getMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.getMultiImage(); expect( @@ -707,7 +729,7 @@ void main() { }); test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.getMultiImage(); await picker.getMultiImage( maxWidth: 10.0, @@ -776,7 +798,7 @@ void main() { }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; + returnValue = ['0', '1']; expect( () => picker.getMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -788,8 +810,8 @@ void main() { ); }); - test('does not accept a invalid imageQuality argument', () { - returnValue = ['0', '1']; + test('does not accept an invalid imageQuality argument', () { + returnValue = ['0', '1']; expect( () => picker.getMultiImage(imageQuality: -1), throwsArgumentError, @@ -934,7 +956,7 @@ void main() { return { 'type': 'image', 'path': '/example/path1', - 'pathList': ['/example/path0', '/example/path1'], + 'pathList': ['/example/path0', '/example/path1'], }; }); final LostDataResponse response = await picker.getLostData(); @@ -979,5 +1001,261 @@ 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, + }), + ], + ); + }); + }); }); } diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart index 7721f66148e0..17233376114a 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart @@ -10,14 +10,14 @@ import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final String expectedStringContents = 'Hello, world!'; +const String expectedStringContents = 'Hello, world!'; final List bytes = utf8.encode(expectedStringContents); -final html.File textFile = html.File([bytes], 'hello.txt'); +final html.File textFile = html.File(>[bytes], 'hello.txt'); final String textFileUrl = html.Url.createObjectUrl(textFile); void main() { group('Create with an objectUrl', () { - final pickedFile = PickedFile(textFileUrl); + final PickedFile pickedFile = PickedFile(textFileUrl); test('Can be read as a string', () async { expect(await pickedFile.readAsString(), equals(expectedStringContents)); 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 d366204c36bf..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 @@ -11,10 +11,10 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final pathPrefix = +final String pathPrefix = Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; -final path = pathPrefix + 'hello.txt'; -final String expectedStringContents = 'Hello, world!'; +final String path = '${pathPrefix}hello.txt'; +const String expectedStringContents = 'Hello, world!'; final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); final File textFile = File(path); final String textFilePath = textFile.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..b8a265568633 --- /dev/null +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -0,0 +1,13 @@ +## 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/connectivity/connectivity_platform_interface/LICENSE b/packages/image_picker/image_picker_windows/LICENSE similarity index 100% rename from packages/connectivity/connectivity_platform_interface/LICENSE rename to packages/image_picker/image_picker_windows/LICENSE 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..7f61053c6e30 --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/README.md @@ -0,0 +1,3 @@ +# image_picker_windows_example + +Demonstrates how to use the image_picker_windows plugin. diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart 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..df1dd49327bd --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -0,0 +1,27 @@ +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.8.0" + +dependencies: + flutter: + sdk: flutter + 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..63eda9b7b59f --- /dev/null +++ b/packages/image_picker/image_picker_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +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) 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..0c6d6fbd6d66 --- /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'); + } + final 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'); + } + final 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 { + final 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..3b6fd922cbea --- /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+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: image_picker + platforms: + windows: + dartPluginClass: ImagePickerWindows + +dependencies: + file_selector_platform_interface: ^2.0.4 + 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 95ba4f27d10a..08ba6dd206c4 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,59 @@ +## 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. + +## 3.0.0 + +* **BREAKING CHANGE** Updates `restorePurchases` to emit an empty list of purchases on StoreKit when there are no purchases to restore (same as Android). + * This change was listed in the CHANGELOG for 2.0.0, but the change was accidentally not included in 2.0.0. + +## 2.0.1 + +* Removes the instructions on initializing the plugin since this functionality is deprecated. + +## 2.0.0 + +* **BREAKING CHANGES**: + * Adds a new `PurchaseStatus` named `canceled`. This means developers can distinguish between an error and user cancellation. + * ~~Updates `restorePurchases` to emit an empty list of purchases on StoreKit when there are no purchases to restore (same as Android).~~ + * Renames `in_app_purchase_ios` to `in_app_purchase_storekit`. + * Renames `InAppPurchaseIosPlatform` to `InAppPurchaseStoreKitPlatform`. + * Renames `InAppPurchaseIosPlatformAddition` to + `InAppPurchaseStoreKitPlatformAddition`. + +* Deprecates the `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` method and `InAppPurchaseAndroidPlatformAddition.enablePendingPurchase` property. +* Adds support for promotional offers on the store_kit_wrappers Dart API. +* Fixes integration tests. +* Updates example app Android compileSdkVersion to 31. + +## 1.0.9 + +* Handle purchases with `PurchaseStatus.restored` correctly in the example App. +* Updated dependencies on `in_app_purchase_android` and `in_app_purchase_ios` to their latest versions (version 0.1.5 and 0.1.3+5 respectively). + ## 1.0.8 * Fix repository link in pubspec.yaml. diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 61803e35ebdc..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: @@ -41,7 +49,7 @@ can start using the plugin. Two basic options are available: 1. A generic, idiomatic Flutter API: [in_app_purchase](https://pub.dev/documentation/in_app_purchase/latest/in_app_purchase/in_app_purchase-library.html). This API supports most use cases for loading and making purchases. -2. Platform-specific Dart APIs: [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_ios/latest/store_kit_wrappers/store_kit_wrappers-library.html) +2. Platform-specific Dart APIs: [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_storekit/latest/store_kit_wrappers/store_kit_wrappers-library.html) and [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html). These APIs expose platform-specific behavior and allow for more fine-tuned control when needed. However, if you use one of these APIs, your @@ -54,7 +62,6 @@ See also the codelab for [in-app purchases in Flutter](https://codelabs.develope This section has examples of code for the following tasks: -* [Initializing the plugin](#initializing-the-plugin) * [Listening to purchase updates](#listening-to-purchase-updates) * [Connecting to the underlying store](#connecting-to-the-underlying-store) * [Loading products for sale](#loading-products-for-sale) @@ -65,27 +72,6 @@ This section has examples of code for the following tasks: * [Accessing platform specific product or purchase properties](#accessing-platform-specific-product-or-purchase-properties) * [Presenting a code redemption sheet (iOS 14)](#presenting-a-code-redemption-sheet-ios-14) -### Initializing the plugin - -The following initialization code is required for Google Play: - -```dart -// Import `in_app_purchase_android.dart` to be able to access the -// `InAppPurchaseAndroidPlatformAddition` class. -import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:flutter/foundation.dart'; - -void main() { - // Inform the plugin that this app supports pending purchases on Android. - // An error will occur on Android if you access the plugin `instance` - // without this call. - if (defaultTargetPlatform == TargetPlatform.android) { - InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); - } - runApp(MyApp()); -} -``` - **Note:** It is not necessary to depend on `com.android.billingclient:billing` in your own app's `android/app/build.gradle` file. If you choose to do so know that conflicts might occur. ### Listening to purchase updates @@ -131,7 +117,7 @@ void _listenToPurchaseUpdated(List purchaseDetailsList) { } else { if (purchaseDetails.status == PurchaseStatus.error) { _handleError(purchaseDetails.error!); - } else if (purchaseDetails.status == PurchaseStatus.purchased || + } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { @@ -175,7 +161,7 @@ List products = response.productDetails; ### Restoring previous purchases Restored purchases will be emitted on the `InAppPurchase.purchaseStream`, make -sure to validate restored purchases following the best practices for each +sure to validate restored purchases following the best practices for each underlying store: * [Verifying App Store purchases](https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store) @@ -212,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. @@ -249,18 +239,18 @@ InAppPurchase.instance ### Confirming subscription price changes -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. +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 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 +When the subscription price is raised, the consumer should approve the price change within 7 days. The official documentation can be found [here](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#zippy=%2Cprice-changes). When the price is lowered the consumer will automatically receive the lower price and does not have to approve the price change. After 7 days the consumer will be notified through email and notifications on Google Play to agree with the new price. App developers have 7 days to explain the consumer that the price is going to change and ask them to accept this change. App developers have to keep track of whether or not the price change is already accepted within the app or in the backend. The [Google Play API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions) can be used to check whether or not the price change is accepted by the consumer by reading the `priceChange` property on a subscription object. -The `InAppPurchaseAndroidPlatformAddition` can be used to show the price change confirmation flow. The additions contain the function `launchPriceChangeConfirmationFlow` which needs the SKU code of the subscription. +The `InAppPurchaseAndroidPlatformAddition` can be used to show the price change confirmation flow. The additions contain the function `launchPriceChangeConfirmationFlow` which needs the SKU code of the subscription. ```dart //import for InAppPurchaseAndroidPlatformAddition @@ -272,7 +262,7 @@ if (Platform.isAndroid) { final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase .getPlatformAddition(); - var priceChangeConfirmationResult = + var priceChangeConfirmationResult = await androidAddition.launchPriceChangeConfirmationFlow( sku: 'purchaseId', ); @@ -286,24 +276,24 @@ if (Platform.isAndroid) { #### Apple App Store (iOS) -When the price of a subscription is raised iOS will also show a popup in the app. +When the price of a subscription is raised iOS will also show a popup in the app. The StoreKit Payment Queue will notify the app that it wants to show a price change confirmation popup. -By default the queue will get the response that it can continue and show the popup. -However, it is possible to prevent this popup via the InAppPurchaseIosPlatformAddition and show the +By default the queue will get the response that it can continue and show the popup. +However, it is possible to prevent this popup via the 'InAppPurchaseStoreKitPlatformAddition' and show the popup at a different time, for example after clicking a button. To know when the App Store wants to show a popup and prevent this from happening a queue delegate can be registered. -The `InAppPurchaseIosPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that +The `InAppPurchaseStoreKitPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that can be used to set a delegate or remove one by setting it to `null`. ```dart -//import for InAppPurchaseIosPlatformAddition -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +//import for InAppPurchaseStoreKitPlatformAddition +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; Future initStoreInfo() async { if (Platform.isIOS) { var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); - await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); } } @@ -311,18 +301,18 @@ Future initStoreInfo() async { Future disposeStore() { if (Platform.isIOS) { var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); + .getPlatformAddition(); await iosPlatformAddition.setDelegate(null); } } ``` -The delegate that is set should implement `SKPaymentQueueDelegateWrapper` and handle `shouldContinueTransaction` and +The delegate that is set should implement `SKPaymentQueueDelegateWrapper` and handle `shouldContinueTransaction` and `shouldShowPriceConsent`. When setting `shouldShowPriceConsent` to false the default popup will not be shown and the app needs to show this later. ```dart // import for SKPaymentQueueDelegateWrapper -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { @override @@ -338,20 +328,20 @@ class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { } ``` -The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurchaseIosPlatformAddition`. This future +The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurchaseStoreKitPlatformAddition`. This future will complete immediately when the dialog is shown. A confirmed transaction will be delivered on the `purchaseStream`. ```dart if (Platform.isIOS) { - var iapIosPlatformAddition = _inAppPurchase - .getPlatformAddition(); - await iapIosPlatformAddition.showPriceConsentIfNeeded(); + var iapStoreKitPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); } ``` ### Accessing platform specific product or purchase properties -The function `_inAppPurchase.queryProductDetails(productIds);` provides a `ProductDetailsResponse` with a -list of purchasable products of type `List`. This `ProductDetails` class is a platform independent class +The function `_inAppPurchase.queryProductDetails(productIds);` provides a `ProductDetailsResponse` with a +list of purchasable products of type `List`. This `ProductDetails` class is a platform independent class containing properties only available on all endorsed platforms. However, in some cases it is necessary to access platform specific properties. The `ProductDetails` instance is of subtype `GooglePlayProductDetails` when the platform is Android and `AppStoreProductDetails` on iOS. Accessing the skuDetails (on Android) or the skProduct (on iOS) provides all the information that is available in the original platform objects. @@ -371,9 +361,9 @@ if (productDetails is GooglePlayProductDetails) { And this is the way to get the subscriptionGroupIdentifier of a subscription on iOS: ```dart //import for AppStoreProductDetails -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; //import for SKProductWrapper -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; if (productDetails is AppStoreProductDetails) { SKProductWrapper skProduct = (productDetails as AppStoreProductDetails).skProduct; @@ -381,10 +371,10 @@ if (productDetails is AppStoreProductDetails) { } ``` -The `purchaseStream` provides objects of type `PurchaseDetails`. PurchaseDetails' provides all -information that is available on all endorsed platforms, such as purchaseID and transactionDate. In addition, it is -possible to access the platform specific properties. The `PurchaseDetails` object is of subtype `GooglePlayPurchaseDetails` -when the platform is Android and `AppStorePurchaseDetails` on iOS. Accessing the billingClientPurchase, resp. +The `purchaseStream` provides objects of type `PurchaseDetails`. PurchaseDetails' provides all +information that is available on all endorsed platforms, such as purchaseID and transactionDate. In addition, it is +possible to access the platform specific properties. The `PurchaseDetails` object is of subtype `GooglePlayPurchaseDetails` +when the platform is Android and `AppStorePurchaseDetails` on iOS. Accessing the billingClientPurchase, resp. skPaymentTransaction provides all the information that is available in the original platform objects. This is an example on how to get the `originalJson` on Android: @@ -403,9 +393,9 @@ if (purchaseDetails is GooglePlayPurchaseDetails) { How to get the `transactionState` of a purchase in iOS: ```dart //import for AppStorePurchaseDetails -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; //import for SKProductWrapper -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; if (purchaseDetails is AppStorePurchaseDetails) { SKPaymentTransactionWrapper skProduct = (purchaseDetails as AppStorePurchaseDetails).skPaymentTransaction; @@ -413,7 +403,7 @@ if (purchaseDetails is AppStorePurchaseDetails) { } ``` -Please note that it is required to import `in_app_purchase_android` and/or `in_app_purchase_ios`. +Please note that it is required to import `in_app_purchase_android` and/or `in_app_purchase_storekit`. ### Presenting a code redemption sheet (iOS 14) @@ -422,18 +412,18 @@ codes that you've set up in App Store Connect. For more information on redeeming offer codes, see [Implementing Offer Codes in Your App](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app). ```dart -InAppPurchaseIosPlatformAddition iosPlatformAddition = - InAppPurchase.getPlatformAddition(); +InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = + InAppPurchase.getPlatformAddition(); iosPlatformAddition.presentCodeRedemptionSheet(); ``` -> **note:** The `InAppPurchaseIosPlatformAddition` is defined in the `in_app_purchase_ios.dart` -> file so you need to import it into the file you will be using `InAppPurchaseIosPlatformAddition`: +> **note:** The `InAppPurchaseStoreKitPlatformAddition` is defined in the `in_app_purchase_storekit.dart` +> file so you need to import it into the file you will be using `InAppPurchaseStoreKitPlatformAddition`: > ```dart -> import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +> import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; > ``` ## 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/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/in_app_purchase/in_app_purchase/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/in_app_purchase/in_app_purchase/build.yaml b/packages/in_app_purchase/in_app_purchase/build.yaml deleted file mode 100644 index e15cf14b85fd..000000000000 --- a/packages/in_app_purchase/in_app_purchase/build.yaml +++ /dev/null @@ -1,7 +0,0 @@ -targets: - $default: - builders: - json_serializable: - options: - any_map: true - create_to_json: true 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 c95804685219..dc8718957291 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 @@ -63,7 +63,7 @@ android { } } - compileSdkVersion 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -107,7 +107,7 @@ flutter { dependencies { implementation 'com.android.billingclient:billing:3.0.2' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:3.6.0' testImplementation 'org.json:json:20180813' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/androidTest/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java similarity index 100% rename from packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/androidTest/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/in_app_purchase/in_app_purchase/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index 1f0955d450f0..000000000000 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/main.m b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/main.m +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/consumable_store.dart b/packages/in_app_purchase/in_app_purchase/example/lib/consumable_store.dart index 4d10a50e1ee8..448efcf40b51 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/consumable_store.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/consumable_store.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'package:shared_preferences/shared_preferences.dart'; +// ignore: avoid_classes_with_only_static_members /// A store of consumable items. /// /// This is a development prototype tha stores consumables in the shared /// preferences. Do not use this in real world apps. class ConsumableStore { static const String _kPrefKey = 'consumables'; - static Future _writes = Future.value(); + static Future _writes = Future.value(); /// Adds a consumable with ID `id` to the store. /// @@ -32,19 +33,19 @@ class ConsumableStore { /// Returns the list of consumables from the store. static Future> load() async { return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ?? - []; + []; } static Future _doSave(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); + final List cached = await load(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); cached.add(id); await prefs.setStringList(_kPrefKey, cached); } static Future _doConsume(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); + final List cached = await load(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); cached.remove(id); await prefs.setStringList(_kPrefKey, cached); } 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 73ecadb3f15d..017b72e976d5 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,25 +4,17 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; 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_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.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() { WidgetsFlutterBinding.ensureInitialized(); - if (defaultTargetPlatform == TargetPlatform.android) { - // For play billing library 2.0 on Android, it is mandatory to call - // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) - // as part of initializing the app. - InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); - } - runApp(_MyApp()); } @@ -41,16 +33,16 @@ const List _kProductIds = [ class _MyApp extends StatefulWidget { @override - _MyAppState createState() => _MyAppState(); + State<_MyApp> createState() => _MyAppState(); } class _MyAppState extends State<_MyApp> { final InAppPurchase _inAppPurchase = InAppPurchase.instance; late StreamSubscription> _subscription; - List _notFoundIds = []; - List _products = []; - List _purchases = []; - List _consumables = []; + List _notFoundIds = []; + List _products = []; + List _purchases = []; + List _consumables = []; bool _isAvailable = false; bool _purchasePending = false; bool _loading = true; @@ -60,11 +52,12 @@ class _MyAppState extends State<_MyApp> { void initState() { final Stream> purchaseUpdated = _inAppPurchase.purchaseStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _subscription = + purchaseUpdated.listen((List purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); }, onDone: () { _subscription.cancel(); - }, onError: (error) { + }, onError: (Object error) { // handle error here. }); initStoreInfo(); @@ -76,10 +69,10 @@ class _MyAppState extends State<_MyApp> { if (!isAvailable) { setState(() { _isAvailable = isAvailable; - _products = []; - _purchases = []; - _notFoundIds = []; - _consumables = []; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; _purchasePending = false; _loading = false; }); @@ -87,21 +80,22 @@ class _MyAppState extends State<_MyApp> { } if (Platform.isIOS) { - var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = + _inAppPurchase + .getPlatformAddition(); await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); } - ProductDetailsResponse productDetailResponse = + final ProductDetailsResponse productDetailResponse = await _inAppPurchase.queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { setState(() { _queryProductError = productDetailResponse.error!.message; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; - _purchases = []; + _purchases = []; _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = []; + _consumables = []; _purchasePending = false; _loading = false; }); @@ -113,16 +107,16 @@ class _MyAppState extends State<_MyApp> { _queryProductError = null; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; - _purchases = []; + _purchases = []; _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = []; + _consumables = []; _purchasePending = false; _loading = false; }); return; } - List consumables = await ConsumableStore.load(); + final List consumables = await ConsumableStore.load(); setState(() { _isAvailable = isAvailable; _products = productDetailResponse.productDetails; @@ -136,8 +130,9 @@ class _MyAppState extends State<_MyApp> { @override void dispose() { if (Platform.isIOS) { - var iosPlatformAddition = _inAppPurchase - .getPlatformAddition(); + final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition = + _inAppPurchase + .getPlatformAddition(); iosPlatformAddition.setDelegate(null); } _subscription.cancel(); @@ -146,11 +141,11 @@ class _MyAppState extends State<_MyApp> { @override Widget build(BuildContext context) { - List stack = []; + final List stack = []; if (_queryProductError == null) { stack.add( ListView( - children: [ + children: [ _buildConnectionCheckTile(), _buildProductList(), _buildConsumableBox(), @@ -166,10 +161,10 @@ class _MyAppState extends State<_MyApp> { if (_purchasePending) { stack.add( Stack( - children: [ + children: const [ Opacity( opacity: 0.3, - child: const ModalBarrier(dismissible: false, color: Colors.grey), + child: ModalBarrier(dismissible: false, color: Colors.grey), ), Center( child: CircularProgressIndicator(), @@ -193,19 +188,19 @@ class _MyAppState extends State<_MyApp> { Card _buildConnectionCheckTile() { if (_loading) { - return Card(child: ListTile(title: const Text('Trying to connect...'))); + return const Card(child: ListTile(title: Text('Trying to connect...'))); } 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') + '.'), + title: + Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'), ); final List children = [storeHeader]; if (!_isAvailable) { - children.addAll([ - Divider(), + children.addAll([ + const Divider(), ListTile( title: Text('Not connected', style: TextStyle(color: ThemeData.light().errorColor)), @@ -219,29 +214,30 @@ class _MyAppState extends State<_MyApp> { Card _buildProductList() { if (_loading) { - return Card( - child: (ListTile( + return const Card( + child: ListTile( leading: CircularProgressIndicator(), - title: Text('Fetching products...')))); + title: Text('Fetching products...'))); } if (!_isAvailable) { - return Card(); + return const Card(); } - final ListTile productHeader = ListTile(title: Text('Products for Sale')); - List productList = []; + const ListTile productHeader = ListTile(title: Text('Products for Sale')); + final List productList = []; if (_notFoundIds.isNotEmpty) { productList.add(ListTile( title: Text('[${_notFoundIds.join(", ")}] not found', style: TextStyle(color: ThemeData.light().errorColor)), - subtitle: Text( + subtitle: const Text( 'This app needs special configuration to run. Please see example/README.md for instructions.'))); } // This loading previous purchases code is just a demo. Please do not use this as it is. // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it. // We recommend that you use your own server to verify the purchase data. - Map purchases = - Map.fromEntries(_purchases.map((PurchaseDetails purchase) { + final Map purchases = + Map.fromEntries( + _purchases.map((PurchaseDetails purchase) { if (purchase.pendingCompletePurchase) { _inAppPurchase.completePurchase(purchase); } @@ -249,86 +245,89 @@ class _MyAppState extends State<_MyApp> { })); productList.addAll(_products.map( (ProductDetails productDetails) { - PurchaseDetails? previousPurchase = purchases[productDetails.id]; + final PurchaseDetails? previousPurchase = purchases[productDetails.id]; return ListTile( - title: Text( - productDetails.title, - ), - subtitle: Text( - productDetails.description, - ), - trailing: previousPurchase != null - ? IconButton( - onPressed: () => confirmPriceChange(context), - icon: 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 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, + applicationUserName: null, + ); + } + + if (productDetails.id == _kConsumableId) { + _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume || Platform.isIOS); + } else { + _inAppPurchase.buyNonConsumable( + purchaseParam: purchaseParam); + } + }, + child: Text(productDetails.price), + ), + ); }, )); return Card( - child: - Column(children: [productHeader, Divider()] + productList)); + child: Column( + children: [productHeader, const Divider()] + productList)); } Card _buildConsumableBox() { if (_loading) { - return Card( - child: (ListTile( + return const Card( + child: ListTile( leading: CircularProgressIndicator(), - title: Text('Fetching consumables...')))); + title: Text('Fetching consumables...'))); } if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { - return Card(); + return const Card(); } - final ListTile consumableHeader = + const ListTile consumableHeader = ListTile(title: Text('Purchased consumables')); final List tokens = _consumables.map((String id) { return GridTile( child: IconButton( - icon: Icon( + icon: const Icon( Icons.stars, size: 42.0, color: Colors.orange, @@ -341,12 +340,12 @@ class _MyAppState extends State<_MyApp> { return Card( child: Column(children: [ consumableHeader, - Divider(), + const Divider(), GridView.count( crossAxisCount: 5, - children: tokens, shrinkWrap: true, - padding: EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16.0), + children: tokens, ) ])); } @@ -361,14 +360,16 @@ class _MyAppState extends State<_MyApp> { child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, - children: [ + children: [ TextButton( - child: 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'), ), ], ), @@ -389,11 +390,11 @@ class _MyAppState extends State<_MyApp> { }); } - void deliverProduct(PurchaseDetails purchaseDetails) async { + Future deliverProduct(PurchaseDetails purchaseDetails) async { // IMPORTANT!! Always verify purchase details before delivering the product. if (purchaseDetails.productID == _kConsumableId) { await ConsumableStore.save(purchaseDetails.purchaseID!); - List consumables = await ConsumableStore.load(); + final List consumables = await ConsumableStore.load(); setState(() { _purchasePending = false; _consumables = consumables; @@ -422,15 +423,17 @@ class _MyAppState extends State<_MyApp> { // handle invalid purchase here if _verifyPurchase` failed. } - void _listenToPurchaseUpdated(List purchaseDetailsList) { - purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { + Future _listenToPurchaseUpdated( + List purchaseDetailsList) async { + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { if (purchaseDetails.status == PurchaseStatus.pending) { showPendingUI(); } else { if (purchaseDetails.status == PurchaseStatus.error) { handleError(purchaseDetails.error!); - } else if (purchaseDetails.status == PurchaseStatus.purchased) { - bool valid = await _verifyPurchase(purchaseDetails); + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { + final bool valid = await _verifyPurchase(purchaseDetails); if (valid) { deliverProduct(purchaseDetails); } else { @@ -450,7 +453,7 @@ class _MyAppState extends State<_MyApp> { await _inAppPurchase.completePurchase(purchaseDetails); } } - }); + } } Future confirmPriceChange(BuildContext context) async { @@ -458,27 +461,28 @@ class _MyAppState extends State<_MyApp> { final InAppPurchaseAndroidPlatformAddition androidAddition = _inAppPurchase .getPlatformAddition(); - var priceChangeConfirmationResult = + final BillingResultWrapper priceChangeConfirmationResult = await androidAddition.launchPriceChangeConfirmationFlow( sku: 'purchaseId', ); if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Price change accepted'), )); } else { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( priceChangeConfirmationResult.debugMessage ?? - "Price change failed with code ${priceChangeConfirmationResult.responseCode}", + 'Price change failed with code ${priceChangeConfirmationResult.responseCode}', ), )); } } if (Platform.isIOS) { - var iapIosPlatformAddition = _inAppPurchase - .getPlatformAddition(); - await iapIosPlatformAddition.showPriceConsentIfNeeded(); + final InAppPurchaseStoreKitPlatformAddition iapStoreKitPlatformAddition = + _inAppPurchase + .getPlatformAddition(); + await iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); } } @@ -495,11 +499,11 @@ class _MyAppState extends State<_MyApp> { if (productDetails.id == _kSilverSubscriptionId && purchases[_kGoldSubscriptionId] != null) { oldSubscription = - purchases[_kGoldSubscriptionId] as GooglePlayPurchaseDetails; + purchases[_kGoldSubscriptionId]! as GooglePlayPurchaseDetails; } else if (productDetails.id == _kGoldSubscriptionId && purchases[_kSilverSubscriptionId] != null) { oldSubscription = - purchases[_kSilverSubscriptionId] as GooglePlayPurchaseDetails; + purchases[_kSilverSubscriptionId]! as GooglePlayPurchaseDetails; } return oldSubscription; } 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 a75aaa689eea..9db9d63c3a79 100644 --- a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml @@ -4,13 +4,11 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - shared_preferences: ^2.0.0 - in_app_purchase: # When depending on this package from a real application you should use: # in_app_purchase: ^x.y.z @@ -18,6 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + shared_preferences: ^2.0.0 dev_dependencies: flutter_driver: @@ -25,7 +24,6 @@ dev_dependencies: integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true 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 4553619af770..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,10 +3,9 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; -import 'package:in_app_purchase_ios/in_app_purchase_ios.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'; export 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart' show @@ -36,7 +35,7 @@ class InAppPurchase implements InAppPurchasePlatformAdditionProvider { if (defaultTargetPlatform == TargetPlatform.android) { InAppPurchaseAndroidPlatform.registerPlatform(); } else if (defaultTargetPlatform == TargetPlatform.iOS) { - InAppPurchaseIosPlatform.registerPlatform(); + InAppPurchaseStoreKitPlatform.registerPlatform(); } _instance = InAppPurchase._(); diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 8b4510b3fce4..8fb69206a95d 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -1,12 +1,12 @@ 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/master/packages/in_app_purchase/in_app_purchase +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: 1.0.8 +version: 3.0.6 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,14 +14,14 @@ flutter: android: default_package: in_app_purchase_android ios: - default_package: in_app_purchase_ios + default_package: in_app_purchase_storekit dependencies: flutter: sdk: flutter + in_app_purchase_android: ^0.2.1 in_app_purchase_platform_interface: ^1.0.0 - in_app_purchase_android: ^0.1.4 - in_app_purchase_ios: ^0.1.1 + in_app_purchase_storekit: ^0.3.0+1 dev_dependencies: flutter_driver: @@ -30,6 +30,5 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 plugin_platform_interface: ^2.0.0 test: ^1.16.0 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 b8c7bd89206b..644d26ed50ad 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 @@ -65,7 +65,7 @@ void main() { test('queryProductDetails', () async { final ProductDetailsResponse response = - await inAppPurchase.queryProductDetails(Set()); + await inAppPurchase.queryProductDetails({}); expect(response.notFoundIDs.isEmpty, true); expect(response.productDetails.isEmpty, true); expect(fakePlatform.log, [ @@ -87,22 +87,24 @@ void main() { }); test('buyConsumable', () async { - final purchaseParam = PurchaseParam(productDetails: productDetails); + final PurchaseParam purchaseParam = + PurchaseParam(productDetails: productDetails); final bool result = await inAppPurchase.buyConsumable( purchaseParam: purchaseParam, ); expect(result, true); expect(fakePlatform.log, [ - isMethodCall('buyConsumable', arguments: { - "purchaseParam": purchaseParam, - "autoConsume": true, + isMethodCall('buyConsumable', arguments: { + 'purchaseParam': purchaseParam, + 'autoConsume': true, }), ]); }); test('buyConsumable with autoConsume=false', () async { - final purchaseParam = PurchaseParam(productDetails: productDetails); + final PurchaseParam purchaseParam = + PurchaseParam(productDetails: productDetails); final bool result = await inAppPurchase.buyConsumable( purchaseParam: purchaseParam, autoConsume: false, @@ -110,9 +112,9 @@ void main() { expect(result, true); expect(fakePlatform.log, [ - isMethodCall('buyConsumable', arguments: { - "purchaseParam": purchaseParam, - "autoConsume": false, + isMethodCall('buyConsumable', arguments: { + 'purchaseParam': purchaseParam, + 'autoConsume': false, }), ]); }); @@ -138,31 +140,33 @@ void main() { class MockInAppPurchasePlatform extends Fake with MockPlatformInterfaceMixin implements InAppPurchasePlatform { - final List log = []; + final List log = []; @override Future isAvailable() { - log.add(MethodCall('isAvailable')); - return Future.value(true); + log.add(const MethodCall('isAvailable')); + return Future.value(true); } @override Stream> get purchaseStream { - log.add(MethodCall('purchaseStream')); - return Stream.empty(); + log.add(const MethodCall('purchaseStream')); + return const Stream>.empty(); } @override Future queryProductDetails(Set identifiers) { - log.add(MethodCall('queryProductDetails')); - return Future.value( - ProductDetailsResponse(productDetails: [], notFoundIDs: [])); + log.add(const MethodCall('queryProductDetails')); + return Future.value(ProductDetailsResponse( + productDetails: [], + notFoundIDs: [], + )); } @override Future buyNonConsumable({required PurchaseParam purchaseParam}) { - log.add(MethodCall('buyNonConsumable')); - return Future.value(true); + log.add(const MethodCall('buyNonConsumable')); + return Future.value(true); } @override @@ -170,22 +174,22 @@ class MockInAppPurchasePlatform extends Fake required PurchaseParam purchaseParam, bool autoConsume = true, }) { - log.add(MethodCall('buyConsumable', { - "purchaseParam": purchaseParam, - "autoConsume": autoConsume, + log.add(MethodCall('buyConsumable', { + 'purchaseParam': purchaseParam, + 'autoConsume': autoConsume, })); - return Future.value(true); + return Future.value(true); } @override Future completePurchase(PurchaseDetails purchase) { - log.add(MethodCall('completePurchase')); - return Future.value(null); + log.add(const MethodCall('completePurchase')); + return Future.value(null); } @override Future restorePurchases({String? applicationUserName}) { - log.add(MethodCall('restorePurchases')); - return Future.value(null); + log.add(const MethodCall('restorePurchases')); + return Future.value(null); } } 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 7a998d0547de..6f476436b3d6 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,76 @@ +## 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. + +## 0.2.2+1 + +* Removes the dependency on `meta`. + +## 0.2.2 + +* Fixes the `purchaseStream` incorrectly reporting `PurchaseStatus.error` when user upgrades subscription by deferred proration mode. + +## 0.2.1 + +* Deprecated the `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` method and `InAppPurchaseAndroidPlatformAddition.enablePendingPurchase` property. Since Google Play no longer accepts App submissions that don't support pending purchases it is no longer necessary to acknowledge this through code. +* Updates example app Android compileSdkVersion to 31. + +## 0.2.0 + +* BREAKING CHANGE : Refactor to handle new `PurchaseStatus` named `canceled`. This means developers + can distinguish between an error and user cancellation. + +## 0.1.6 + +* Require Dart SDK >= 2.14. +* Update `json_annotation` dependency to `^4.3.0`. + +## 0.1.5+1 + +* Fix a broken link in the README. + +## 0.1.5 + +* Introduced the `SkuDetailsWrapper.introductoryPriceAmountMicros` field of the correct type (`int`) and deprecated the `SkuDetailsWrapper.introductoryPriceMicros` field. +* Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. + ## 0.1.4+7 * Ensure that the `SkuDetailsWrapper.introductoryPriceMicros` is populated correctly. 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 dcf5256e3bbc..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,9 +21,9 @@ 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/in_app_purchase +[1]: https://pub.dev/packages/in_app_purchase [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [3]: https://pub.dev/packages/in_app_purchase_android/install diff --git a/packages/in_app_purchase/in_app_purchase_android/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase_android/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml 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 656f7c34bf7a..663a4dfad46e 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 @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { minSdkVersion 16 @@ -52,11 +52,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:20220320' + testImplementation 'org.mockito:mockito-core:4.5.1' + 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/BillingClientFactory.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index 7b21cbf2e6f5..81fdf27be88e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java @@ -17,10 +17,7 @@ interface BillingClientFactory { * * @param context The context used to create the {@link BillingClient}. * @param channel The method channel used to create the {@link BillingClient}. - * @param enablePendingPurchases Whether to enable pending purchases. Throws an exception if it is - * false. * @return The {@link BillingClient} object that is created. */ - BillingClient createBillingClient( - @NonNull Context context, @NonNull MethodChannel channel, boolean enablePendingPurchases); + BillingClient createBillingClient(@NonNull Context context, @NonNull MethodChannel channel); } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index c256d2c59551..6d2639840a74 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java @@ -12,12 +12,9 @@ final class BillingClientFactoryImpl implements BillingClientFactory { @Override - public BillingClient createBillingClient( - Context context, MethodChannel channel, boolean enablePendingPurchases) { - BillingClient.Builder builder = BillingClient.newBuilder(context); - if (enablePendingPurchases) { - builder.enablePendingPurchases(); - } + public BillingClient createBillingClient(Context context, MethodChannel channel) { + BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases(); + return builder.setListener(new PluginPurchaseListener(channel)).build(); } } 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 5b58808b2b49..ab12b2db8630 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; @@ -110,10 +114,7 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { isReady(result); break; case InAppPurchasePlugin.MethodNames.START_CONNECTION: - startConnection( - (int) call.argument("handle"), - (boolean) call.argument("enablePendingPurchases"), - result); + startConnection((int) call.argument("handle"), result); break; case InAppPurchasePlugin.MethodNames.END_CONNECTION: endConnection(result); @@ -134,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: @@ -152,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(); } @@ -177,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)) { @@ -211,7 +220,6 @@ private void launchBillingFlow( if (billingClientError(result)) { return; } - SkuDetails skuDetails = cachedSkus.get(sku); if (skuDetails == null) { result.error( @@ -258,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()))); @@ -289,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("purchaseList", fromPurchasesList(purchasesList)); + result.success(serialized); + } + }); } private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Result result) { @@ -305,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( @@ -319,12 +346,18 @@ public void onPurchaseHistoryResponse( }); } - private void startConnection( - final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) { + 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, enablePendingPurchases); + billingClient = billingClientFactory.createBillingClient(applicationContext, methodChannel); } billingClient.startConnection( 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 6f9256cd07bd..e99ff46dd2cc 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,21 +13,19 @@ 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; @@ -56,9 +54,9 @@ 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.QueryPurchaseHistoryParams; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; import com.android.billingclient.api.SkuDetailsResponseListener; @@ -90,10 +88,7 @@ public class MethodCallHandlerTest { @Before public void setUp() { MockitoAnnotations.openMocks(this); - factory = - (@NonNull Context context, - @NonNull MethodChannel channel, - boolean enablePendingPurchases) -> mockBillingClient; + factory = (@NonNull Context context, @NonNull MethodChannel channel) -> mockBillingClient; methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); when(mockActivityPluginBinding.getActivity()).thenReturn(activity); } @@ -153,7 +148,6 @@ public void startConnection() { public void startConnection_multipleCalls() { Map arguments = new HashMap<>(); arguments.put("handle", 1); - arguments.put("enablePendingPurchases", true); MethodCall call = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); @@ -191,7 +185,6 @@ public void endConnection() { final int disconnectCallbackHandle = 22; Map arguments = new HashMap<>(); arguments.put("handle", disconnectCallbackHandle); - arguments.put("enablePendingPurchases", true); MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); @@ -299,7 +292,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()); @@ -332,8 +324,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)); @@ -385,8 +375,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()); @@ -418,7 +406,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()); @@ -456,10 +443,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()); @@ -500,6 +483,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 @@ -558,31 +578,6 @@ public void launchBillingFlow_oldSkuNotFound() { verify(result, never()).success(any()); } - @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); - - HashMap arguments = new HashMap<>(); - arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, 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()); - } - @Test public void queryPurchases_clientDisconnected() { // Prepare the launch call after disconnecting the client @@ -590,7 +585,7 @@ public void queryPurchases_clientDisconnected() { 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()); @@ -618,7 +613,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(); @@ -865,7 +860,6 @@ public void launchPriceChangeConfirmationFlow_withoutBillingClient_returnsUnavai private ArgumentCaptor mockStartConnection() { Map arguments = new HashMap<>(); arguments.put("handle", 1); - arguments.put("enablePendingPurchases", true); MethodCall call = new MethodCall(START_CONNECTION, arguments); ArgumentCaptor captor = ArgumentCaptor.forClass(BillingClientStateListener.class); @@ -880,7 +874,6 @@ private void establishConnectedBillingClient( if (arguments == null) { arguments = new HashMap<>(); arguments.put("handle", 1); - arguments.put("enablePendingPurchases", true); } if (result == null) { result = mock(Result.class); 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 _writes = Future.value(); + static Future _writes = Future.value(); /// Adds a consumable with ID `id` to the store. /// @@ -32,19 +33,19 @@ class ConsumableStore { /// Returns the list of consumables from the store. static Future> load() async { return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ?? - []; + []; } static Future _doSave(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); + final List cached = await load(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); cached.add(id); await prefs.setStringList(_kPrefKey, cached); } static Future _doConsume(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); + final List cached = await load(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); cached.remove(id); await prefs.setStringList(_kPrefKey, cached); } diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index 126734187380..337811a9acfd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -15,11 +15,6 @@ import 'consumable_store.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - // For play billing library 2.0 on Android, it is mandatory to call - // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) - // as part of initializing the app. - InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); - // When using the Android plugin directly it is mandatory to register // the plugin as default instance as part of initializing the app. InAppPurchaseAndroidPlatform.registerPlatform(); @@ -42,17 +37,17 @@ const List _kProductIds = [ class _MyApp extends StatefulWidget { @override - _MyAppState createState() => _MyAppState(); + State<_MyApp> createState() => _MyAppState(); } class _MyAppState extends State<_MyApp> { final InAppPurchasePlatform _inAppPurchasePlatform = InAppPurchasePlatform.instance; late StreamSubscription> _subscription; - List _notFoundIds = []; - List _products = []; - List _purchases = []; - List _consumables = []; + List _notFoundIds = []; + List _products = []; + List _purchases = []; + List _consumables = []; bool _isAvailable = false; bool _purchasePending = false; bool _loading = true; @@ -62,11 +57,12 @@ class _MyAppState extends State<_MyApp> { void initState() { final Stream> purchaseUpdated = _inAppPurchasePlatform.purchaseStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _subscription = + purchaseUpdated.listen((List purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); }, onDone: () { _subscription.cancel(); - }, onError: (error) { + }, onError: (Object error) { // handle error here. }); initStoreInfo(); @@ -78,26 +74,26 @@ class _MyAppState extends State<_MyApp> { if (!isAvailable) { setState(() { _isAvailable = isAvailable; - _products = []; - _purchases = []; - _notFoundIds = []; - _consumables = []; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; _purchasePending = false; _loading = false; }); return; } - ProductDetailsResponse productDetailResponse = + final ProductDetailsResponse productDetailResponse = await _inAppPurchasePlatform.queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { setState(() { _queryProductError = productDetailResponse.error!.message; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; - _purchases = []; + _purchases = []; _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = []; + _consumables = []; _purchasePending = false; _loading = false; }); @@ -109,9 +105,9 @@ class _MyAppState extends State<_MyApp> { _queryProductError = null; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; - _purchases = []; + _purchases = []; _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = []; + _consumables = []; _purchasePending = false; _loading = false; }); @@ -120,7 +116,7 @@ class _MyAppState extends State<_MyApp> { await _inAppPurchasePlatform.restorePurchases(); - List consumables = await ConsumableStore.load(); + final List consumables = await ConsumableStore.load(); setState(() { _isAvailable = isAvailable; _products = productDetailResponse.productDetails; @@ -139,11 +135,11 @@ class _MyAppState extends State<_MyApp> { @override Widget build(BuildContext context) { - List stack = []; + final List stack = []; if (_queryProductError == null) { stack.add( ListView( - children: [ + children: [ _buildConnectionCheckTile(), _buildProductList(), _buildConsumableBox(), @@ -159,10 +155,10 @@ class _MyAppState extends State<_MyApp> { if (_purchasePending) { stack.add( Stack( - children: [ + children: const [ Opacity( opacity: 0.3, - child: const ModalBarrier(dismissible: false, color: Colors.grey), + child: ModalBarrier(dismissible: false, color: Colors.grey), ), Center( child: CircularProgressIndicator(), @@ -186,19 +182,19 @@ class _MyAppState extends State<_MyApp> { Card _buildConnectionCheckTile() { if (_loading) { - return Card(child: ListTile(title: const Text('Trying to connect...'))); + return const Card(child: ListTile(title: Text('Trying to connect...'))); } 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') + '.'), + title: + Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'), ); final List children = [storeHeader]; if (!_isAvailable) { - children.addAll([ - Divider(), + children.addAll([ + const Divider(), ListTile( title: Text('Not connected', style: TextStyle(color: ThemeData.light().errorColor)), @@ -212,29 +208,30 @@ class _MyAppState extends State<_MyApp> { Card _buildProductList() { if (_loading) { - return Card( - child: (ListTile( + return const Card( + child: ListTile( leading: CircularProgressIndicator(), - title: Text('Fetching products...')))); + title: Text('Fetching products...'))); } if (!_isAvailable) { - return Card(); + return const Card(); } - final ListTile productHeader = ListTile(title: Text('Products for Sale')); - List productList = []; + const ListTile productHeader = ListTile(title: Text('Products for Sale')); + final List productList = []; if (_notFoundIds.isNotEmpty) { productList.add(ListTile( title: Text('[${_notFoundIds.join(", ")}] not found', style: TextStyle(color: ThemeData.light().errorColor)), - subtitle: Text( + subtitle: const Text( 'This app needs special configuration to run. Please see example/README.md for instructions.'))); } // This loading previous purchases code is just a demo. Please do not use this as it is. // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it. // We recommend that you use your own server to verify the purchase data. - Map purchases = - Map.fromEntries(_purchases.map((PurchaseDetails purchase) { + final Map purchases = + Map.fromEntries( + _purchases.map((PurchaseDetails purchase) { if (purchase.pendingCompletePurchase) { _inAppPurchasePlatform.completePurchase(purchase); } @@ -242,7 +239,7 @@ class _MyAppState extends State<_MyApp> { })); productList.addAll(_products.map( (ProductDetails productDetails) { - PurchaseDetails? previousPurchase = purchases[productDetails.id]; + final PurchaseDetails? previousPurchase = purchases[productDetails.id]; return ListTile( title: Text( productDetails.title, @@ -254,22 +251,23 @@ class _MyAppState extends State<_MyApp> { ? IconButton( onPressed: () { final InAppPurchaseAndroidPlatformAddition addition = - InAppPurchasePlatformAddition.instance + InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; - var skuDetails = + final SkuDetailsWrapper skuDetails = (productDetails as GooglePlayProductDetails) .skuDetails; addition .launchPriceChangeConfirmationFlow( sku: skuDetails.sku) - .then((value) => print( - "confirmationResponse: ${value.responseCode}")); + .then((BillingResultWrapper value) => print( + 'confirmationResponse: ${value.responseCode}')); }, - icon: Icon(Icons.upgrade)) + 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: () { @@ -277,10 +275,11 @@ class _MyAppState extends State<_MyApp> { // verify the latest status of you your subscription by using server side receipt validation // and update the UI accordingly. The subscription purchase status shown // inside the app may not be accurate. - final oldSubscription = _getOldSubscription( - productDetails as GooglePlayProductDetails, - purchases); - GooglePlayPurchaseParam purchaseParam = + final GooglePlayPurchaseDetails? oldSubscription = + _getOldSubscription( + productDetails as GooglePlayProductDetails, + purchases); + final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( productDetails: productDetails, applicationUserName: null, @@ -299,31 +298,32 @@ class _MyAppState extends State<_MyApp> { purchaseParam: purchaseParam); } }, + child: Text(productDetails.price), )); }, )); return Card( - child: - Column(children: [productHeader, Divider()] + productList)); + child: Column( + children: [productHeader, const Divider()] + productList)); } Card _buildConsumableBox() { if (_loading) { - return Card( - child: (ListTile( + return const Card( + child: ListTile( leading: CircularProgressIndicator(), - title: Text('Fetching consumables...')))); + title: Text('Fetching consumables...'))); } if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { - return Card(); + return const Card(); } - final ListTile consumableHeader = + const ListTile consumableHeader = ListTile(title: Text('Purchased consumables')); final List tokens = _consumables.map((String id) { return GridTile( child: IconButton( - icon: Icon( + icon: const Icon( Icons.stars, size: 42.0, color: Colors.orange, @@ -336,12 +336,12 @@ class _MyAppState extends State<_MyApp> { return Card( child: Column(children: [ consumableHeader, - Divider(), + const Divider(), GridView.count( crossAxisCount: 5, - children: tokens, shrinkWrap: true, - padding: EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16.0), + children: tokens, ) ])); } @@ -360,11 +360,11 @@ class _MyAppState extends State<_MyApp> { }); } - void deliverProduct(PurchaseDetails purchaseDetails) async { + Future deliverProduct(PurchaseDetails purchaseDetails) async { // IMPORTANT!! Always verify purchase details before delivering the product. if (purchaseDetails.productID == _kConsumableId) { await ConsumableStore.save(purchaseDetails.purchaseID!); - List consumables = await ConsumableStore.load(); + final List consumables = await ConsumableStore.load(); setState(() { _purchasePending = false; _consumables = consumables; @@ -393,8 +393,9 @@ class _MyAppState extends State<_MyApp> { // handle invalid purchase here if _verifyPurchase` failed. } - void _listenToPurchaseUpdated(List purchaseDetailsList) { - purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { + Future _listenToPurchaseUpdated( + List purchaseDetailsList) async { + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { if (purchaseDetails.status == PurchaseStatus.pending) { showPendingUI(); } else { @@ -402,7 +403,7 @@ class _MyAppState extends State<_MyApp> { handleError(purchaseDetails.error!); } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { - bool valid = await _verifyPurchase(purchaseDetails); + final bool valid = await _verifyPurchase(purchaseDetails); if (valid) { deliverProduct(purchaseDetails); } else { @@ -413,7 +414,7 @@ class _MyAppState extends State<_MyApp> { if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) { final InAppPurchaseAndroidPlatformAddition addition = - InAppPurchasePlatformAddition.instance + InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; await addition.consumePurchase(purchaseDetails); @@ -423,7 +424,7 @@ class _MyAppState extends State<_MyApp> { await _inAppPurchasePlatform.completePurchase(purchaseDetails); } } - }); + } } GooglePlayPurchaseDetails? _getOldSubscription( @@ -440,31 +441,31 @@ class _MyAppState extends State<_MyApp> { if (productDetails.id == _kSilverSubscriptionId && purchases[_kGoldSubscriptionId] != null) { oldSubscription = - purchases[_kGoldSubscriptionId] as GooglePlayPurchaseDetails; + purchases[_kGoldSubscriptionId]! as GooglePlayPurchaseDetails; } else if (productDetails.id == _kGoldSubscriptionId && purchases[_kSilverSubscriptionId] != null) { oldSubscription = - purchases[_kSilverSubscriptionId] as GooglePlayPurchaseDetails; + purchases[_kSilverSubscriptionId]! as GooglePlayPurchaseDetails; } return oldSubscription; } } class _FeatureCard extends StatelessWidget { + _FeatureCard({Key? key}) : super(key: key); + final InAppPurchaseAndroidPlatformAddition addition = - InAppPurchasePlatformAddition.instance + InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; - _FeatureCard({Key? key}) : super(key: key); - @override Widget build(BuildContext context) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ListTile(title: Text('Available features')), - Divider(), + const ListTile(title: Text('Available features')), + const Divider(), for (BillingClientFeature feature in BillingClientFeature.values) _buildFeatureWidget(feature), ])); @@ -473,9 +474,9 @@ class _FeatureCard extends StatelessWidget { Widget _buildFeatureWidget(BillingClientFeature feature) { return FutureBuilder( future: addition.isFeatureSupported(feature), - builder: (context, snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { Color color = Colors.grey; - bool? data = snapshot.data; + final bool? data = snapshot.data; if (data != null) { color = data ? Colors.green : Colors.red; } 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 f27261669438..0d37b3df1ee5 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,12 +4,11 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - shared_preferences: ^2.0.0 in_app_purchase_android: # When depending on this package from a real application you should use: # in_app_purchase_android: ^x.y.z @@ -17,15 +16,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: ../ - in_app_purchase_platform_interface: ^1.0.0 + shared_preferences: ^2.0.0 dev_dependencies: flutter_driver: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true 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 4393d1d72eaf..70343fcfff0b 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 @@ -3,14 +3,15 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:flutter/services.dart'; + import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:json_annotation/json_annotation.dart'; + import '../../billing_client_wrappers.dart'; import '../channel.dart'; -import 'purchase_wrapper.dart'; -import 'sku_details_wrapper.dart'; -import 'enum_converters.dart'; + +part 'billing_client_wrapper.g.dart'; /// Method identifier for the OnPurchaseUpdated method channel method. @visibleForTesting @@ -35,7 +36,8 @@ const String _kOnBillingServiceDisconnected = /// /// Wraps a /// [`PurchasesUpdatedListener`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener.html). -typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult); +typedef PurchasesUpdatedListener = void Function( + PurchasesResultWrapper purchasesResult); /// This class can be used directly instead of [InAppPurchaseConnection] to call /// Play-specific billing APIs. @@ -50,12 +52,12 @@ typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult); /// some minor changes to account for language differences. Callbacks have been /// converted to futures where appropriate. class BillingClient { - bool _enablePendingPurchases = false; - /// Creates a billing client. BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { channel.setMethodCallHandler(callHandler); - _callbacks[kOnPurchasesUpdated] = [onPurchasesUpdated]; + _callbacks[kOnPurchasesUpdated] = [ + onPurchasesUpdated + ]; } // Occasionally methods in the native layer require a Dart callback to be @@ -66,7 +68,7 @@ class BillingClient { // matching callback here to remember, and then once its twin is triggered it // sends the handle back over the platform channel. We then access that handle // in this array and call it in Dart code. See also [_callHandler]. - Map> _callbacks = >{}; + final Map> _callbacks = >{}; /// Calls /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) @@ -79,14 +81,14 @@ class BillingClient { /// Enable the [BillingClientWrapper] to handle pending purchases. /// - /// Play requires that you call this method when initializing your application. - /// It is to acknowledge your application has been updated to support pending purchases. - /// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending) - /// for more details. - /// - /// Failure to call this method before any other method in the [startConnection] will throw an exception. + /// **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.') void enablePendingPurchases() { - _enablePendingPurchases = true; + // No-op, until it is time to completely remove this method from the API. } /// Calls @@ -102,17 +104,14 @@ class BillingClient { Future startConnection( {required OnBillingServiceDisconnected onBillingServiceDisconnected}) async { - assert(_enablePendingPurchases, - 'enablePendingPurchases() must be called before calling startConnection'); - List disconnectCallbacks = - _callbacks[_kOnBillingServiceDisconnected] ??= []; + final List disconnectCallbacks = + _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); return BillingResultWrapper.fromJson((await channel .invokeMapMethod( - "BillingClient#startConnection(BillingClientStateListener)", + 'BillingClient#startConnection(BillingClientStateListener)', { 'handle': disconnectCallbacks.length - 1, - 'enablePendingPurchases': _enablePendingPurchases })) ?? {}); } @@ -125,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()', null); } /// Returns a list of [SkuDetailsWrapper]s that have [SkuDetailsWrapper.sku] @@ -140,7 +139,7 @@ class BillingClient { Future querySkuDetails( {required SkuType skuType, required List skusList}) async { final Map arguments = { - 'skuType': SkuTypeConverter().toJson(skuType), + 'skuType': const SkuTypeConverter().toJson(skuType), 'skusList': skusList }; return SkuDetailsResponseWrapper.fromJson((await channel.invokeMapMethod< @@ -201,7 +200,7 @@ class BillingClient { 'obfuscatedProfileId': obfuscatedProfileId, 'oldSku': oldSku, 'purchaseToken': purchaseToken, - 'prorationMode': ProrationModeConverter().toJson(prorationMode ?? + 'prorationMode': const ProrationModeConverter().toJson(prorationMode ?? ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) }; return BillingResultWrapper.fromJson( @@ -227,7 +226,7 @@ class BillingClient { return PurchasesResultWrapper.fromJson((await channel .invokeMapMethod( 'BillingClient#queryPurchases(String)', { - 'skuType': SkuTypeConverter().toJson(skuType) + 'skuType': const SkuTypeConverter().toJson(skuType) })) ?? {}); } @@ -251,7 +250,7 @@ class BillingClient { String, dynamic>( 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', { - 'skuType': SkuTypeConverter().toJson(skuType) + 'skuType': const SkuTypeConverter().toJson(skuType) })) ?? {}); } @@ -304,9 +303,9 @@ class BillingClient { /// Checks if the specified feature or capability is supported by the Play Store. /// Call this to check if a [BillingClientFeature] is supported by the device. Future isFeatureSupported(BillingClientFeature feature) async { - var result = await channel.invokeMethod( + final bool? result = await channel.invokeMethod( 'BillingClient#isFeatureSupported(String)', { - 'feature': BillingClientFeatureConverter().toJson(feature), + 'feature': const BillingClientFeatureConverter().toJson(feature), }); return result ?? false; } @@ -341,10 +340,10 @@ class BillingClient { final PurchasesUpdatedListener listener = _callbacks[kOnPurchasesUpdated]!.first as PurchasesUpdatedListener; listener(PurchasesResultWrapper.fromJson( - call.arguments.cast())); + (call.arguments as Map).cast())); break; case _kOnBillingServiceDisconnected: - final int handle = call.arguments['handle']; + final int handle = call.arguments['handle'] as int; await _callbacks[_kOnBillingServiceDisconnected]![handle](); break; } @@ -356,7 +355,7 @@ class BillingClient { /// Wraps /// [`com.android.billingclient.api.BillingClientStateListener.onServiceDisconnected()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClientStateListener.html#onBillingServiceDisconnected()) /// to call back on `BillingClient` disconnect. -typedef void OnBillingServiceDisconnected(); +typedef OnBillingServiceDisconnected = void Function(); /// Possible `BillingClient` response statuses. /// @@ -364,6 +363,7 @@ typedef void OnBillingServiceDisconnected(); /// [`BillingClient.BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse). /// See the `BillingResponse` docs for more explanation of the different /// constants. +@JsonEnum(alwaysCreate: true) enum BillingResponse { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -418,11 +418,32 @@ enum BillingResponse { itemNotOwned, } +/// Serializer for [BillingResponse]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingResponseConverter()`. +class BillingResponseConverter implements JsonConverter { + /// Default const constructor. + const BillingResponseConverter(); + + @override + BillingResponse fromJson(int? json) { + if (json == null) { + return BillingResponse.error; + } + return $enumDecode(_$BillingResponseEnumMap, json); + } + + @override + int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; +} + /// Enum representing potential [SkuDetailsWrapper.type]s. /// /// Wraps /// [`BillingClient.SkuType`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.SkuType) /// See the linked documentation for an explanation of the different constants. +@JsonEnum(alwaysCreate: true) enum SkuType { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -437,6 +458,26 @@ enum SkuType { subs, } +/// Serializer for [SkuType]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SkuTypeConverter()`. +class SkuTypeConverter implements JsonConverter { + /// Default const constructor. + const SkuTypeConverter(); + + @override + SkuType fromJson(String? json) { + if (json == null) { + return SkuType.inapp; + } + return $enumDecode(_$SkuTypeEnumMap, json); + } + + @override + String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; +} + /// Enum representing the proration mode. /// /// When upgrading or downgrading a subscription, set this mode to provide details @@ -444,6 +485,7 @@ enum SkuType { /// /// Wraps [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode) /// See the linked documentation for an explanation of the different constants. +@JsonEnum(alwaysCreate: true) enum ProrationMode { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -477,7 +519,28 @@ enum ProrationMode { deferred, } +/// Serializer for [ProrationMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@ProrationModeConverter()`. +class ProrationModeConverter implements JsonConverter { + /// Default const constructor. + const ProrationModeConverter(); + + @override + ProrationMode fromJson(int? json) { + if (json == null) { + return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; + } + return $enumDecode(_$ProrationModeEnumMap, json); + } + + @override + int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; +} + /// Features/capabilities supported by [BillingClient.isFeatureSupported()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType). +@JsonEnum(alwaysCreate: true) enum BillingClientFeature { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for @@ -504,3 +567,24 @@ enum BillingClientFeature { @JsonValue('subscriptionsUpdate') subscriptionsUpdate } + +/// Serializer for [BillingClientFeature]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingClientFeatureConverter()`. +class BillingClientFeatureConverter + implements JsonConverter { + /// Default const constructor. + const BillingClientFeatureConverter(); + + @override + BillingClientFeature fromJson(String json) { + return $enumDecode( + _$BillingClientFeatureEnumMap.cast(), + json); + } + + @override + String toJson(BillingClientFeature object) => + _$BillingClientFeatureEnumMap[object]!; +} 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 new file mode 100644 index 000000000000..efe7656d2138 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'billing_client_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +const _$BillingResponseEnumMap = { + BillingResponse.serviceTimeout: -3, + BillingResponse.featureNotSupported: -2, + BillingResponse.serviceDisconnected: -1, + BillingResponse.ok: 0, + BillingResponse.userCanceled: 1, + BillingResponse.serviceUnavailable: 2, + BillingResponse.billingUnavailable: 3, + BillingResponse.itemUnavailable: 4, + BillingResponse.developerError: 5, + BillingResponse.error: 6, + BillingResponse.itemAlreadyOwned: 7, + BillingResponse.itemNotOwned: 8, +}; + +const _$SkuTypeEnumMap = { + SkuType.inapp: 'inapp', + SkuType.subs: 'subs', +}; + +const _$ProrationModeEnumMap = { + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, + ProrationMode.immediateWithTimeProration: 1, + ProrationMode.immediateAndChargeProratedPrice: 2, + ProrationMode.immediateWithoutProration: 3, + ProrationMode.deferred: 4, +}; + +const _$BillingClientFeatureEnumMap = { + BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr', + BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation', + BillingClientFeature.subscriptions: 'subscriptions', + BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr', + BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate', +}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart deleted file mode 100644 index 7ff333098fcc..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart +++ /dev/null @@ -1,142 +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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import '../../billing_client_wrappers.dart'; - -part 'enum_converters.g.dart'; - -/// Serializer for [BillingResponse]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@BillingResponseConverter()`. -class BillingResponseConverter implements JsonConverter { - /// Default const constructor. - const BillingResponseConverter(); - - @override - BillingResponse fromJson(int? json) { - if (json == null) { - return BillingResponse.error; - } - return _$enumDecode( - _$BillingResponseEnumMap.cast(), json); - } - - @override - int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; -} - -/// Serializer for [SkuType]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SkuTypeConverter()`. -class SkuTypeConverter implements JsonConverter { - /// Default const constructor. - const SkuTypeConverter(); - - @override - SkuType fromJson(String? json) { - if (json == null) { - return SkuType.inapp; - } - return _$enumDecode( - _$SkuTypeEnumMap.cast(), json); - } - - @override - String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; -} - -/// Serializer for [ProrationMode]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@ProrationModeConverter()`. -class ProrationModeConverter implements JsonConverter { - /// Default const constructor. - const ProrationModeConverter(); - - @override - ProrationMode fromJson(int? json) { - if (json == null) { - return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; - } - return _$enumDecode( - _$ProrationModeEnumMap.cast(), json); - } - - @override - int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; -} - -/// Serializer for [PurchaseStateWrapper]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@PurchaseStateConverter()`. -class PurchaseStateConverter - implements JsonConverter { - /// Default const constructor. - const PurchaseStateConverter(); - - @override - PurchaseStateWrapper fromJson(int? json) { - if (json == null) { - return PurchaseStateWrapper.unspecified_state; - } - return _$enumDecode( - _$PurchaseStateWrapperEnumMap.cast(), - json); - } - - @override - int toJson(PurchaseStateWrapper object) => - _$PurchaseStateWrapperEnumMap[object]!; - - /// Converts the purchase state stored in `object` to a [PurchaseStatus]. - /// - /// [PurchaseStateWrapper.unspecified_state] is mapped to [PurchaseStatus.error]. - PurchaseStatus toPurchaseStatus(PurchaseStateWrapper object) { - switch (object) { - case PurchaseStateWrapper.pending: - return PurchaseStatus.pending; - case PurchaseStateWrapper.purchased: - return PurchaseStatus.purchased; - case PurchaseStateWrapper.unspecified_state: - return PurchaseStatus.error; - } - } -} - -/// Serializer for [BillingClientFeature]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@BillingClientFeatureConverter()`. -class BillingClientFeatureConverter - implements JsonConverter { - /// Default const constructor. - const BillingClientFeatureConverter(); - - @override - BillingClientFeature fromJson(String json) { - return _$enumDecode( - _$BillingClientFeatureEnumMap.cast(), - json); - } - - @override - String toJson(BillingClientFeature object) => - _$BillingClientFeatureEnumMap[object]!; -} - -// Define a class so we generate serializer helper methods for the enums -@JsonSerializable() -class _SerializedEnums { - late BillingResponse response; - late SkuType type; - late PurchaseStateWrapper purchaseState; - late ProrationMode prorationMode; - late BillingClientFeature billingClientFeature; -} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart deleted file mode 100644 index 8d667d035196..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart +++ /dev/null @@ -1,97 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'enum_converters.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_SerializedEnums _$_SerializedEnumsFromJson(Map json) { - return _SerializedEnums() - ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) - ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) - ..purchaseState = - _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']) - ..prorationMode = - _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']) - ..billingClientFeature = _$enumDecode( - _$BillingClientFeatureEnumMap, json['billingClientFeature']); -} - -Map _$_SerializedEnumsToJson(_SerializedEnums instance) => - { - 'response': _$BillingResponseEnumMap[instance.response], - 'type': _$SkuTypeEnumMap[instance.type], - 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], - 'prorationMode': _$ProrationModeEnumMap[instance.prorationMode], - 'billingClientFeature': - _$BillingClientFeatureEnumMap[instance.billingClientFeature], - }; - -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - -const _$BillingResponseEnumMap = { - BillingResponse.serviceTimeout: -3, - BillingResponse.featureNotSupported: -2, - BillingResponse.serviceDisconnected: -1, - BillingResponse.ok: 0, - BillingResponse.userCanceled: 1, - BillingResponse.serviceUnavailable: 2, - BillingResponse.billingUnavailable: 3, - BillingResponse.itemUnavailable: 4, - BillingResponse.developerError: 5, - BillingResponse.error: 6, - BillingResponse.itemAlreadyOwned: 7, - BillingResponse.itemNotOwned: 8, -}; - -const _$SkuTypeEnumMap = { - SkuType.inapp: 'inapp', - SkuType.subs: 'subs', -}; - -const _$PurchaseStateWrapperEnumMap = { - PurchaseStateWrapper.unspecified_state: 0, - PurchaseStateWrapper.purchased: 1, - PurchaseStateWrapper.pending: 2, -}; - -const _$ProrationModeEnumMap = { - ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, - ProrationMode.immediateWithTimeProration: 1, - ProrationMode.immediateAndChargeProratedPrice: 2, - ProrationMode.immediateWithoutProration: 3, - ProrationMode.deferred: 4, -}; - -const _$BillingClientFeatureEnumMap = { - BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr', - BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation', - BillingClientFeature.subscriptions: 'subscriptions', - BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr', - BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate', -}; 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 374c26ab4a7a..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,11 +2,10 @@ // 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'; -import 'enum_converters.dart'; + import 'billing_client_wrapper.dart'; import 'sku_details_wrapper.dart'; @@ -24,16 +23,18 @@ part 'purchase_wrapper.g.dart'; /// This wraps [`com.android.billlingclient.api.Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase) @JsonSerializable() @PurchaseStateConverter() +@immutable class PurchaseWrapper { /// Creates a purchase wrapper with the given purchase details. @visibleForTesting - PurchaseWrapper({ + const PurchaseWrapper({ required this.orderId, required this.packageName, 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, @@ -41,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) => @@ -49,23 +50,27 @@ class PurchaseWrapper { @override bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchaseWrapper typedOther = other as PurchaseWrapper; - return typedOther.orderId == orderId && - typedOther.packageName == packageName && - typedOther.purchaseTime == purchaseTime && - typedOther.purchaseToken == purchaseToken && - typedOther.signature == signature && - typedOther.sku == sku && - typedOther.isAutoRenewing == isAutoRenewing && - typedOther.originalJson == originalJson && - typedOther.isAcknowledged == isAcknowledged && - typedOther.purchaseState == purchaseState; + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is PurchaseWrapper && + other.orderId == orderId && + other.packageName == packageName && + other.purchaseTime == purchaseTime && + other.purchaseToken == purchaseToken && + other.signature == signature && + other.sku == sku && + other.isAutoRenewing == isAutoRenewing && + other.originalJson == originalJson && + other.isAcknowledged == isAcknowledged && + other.purchaseState == purchaseState; } @override - int get hashCode => hashValues( + int get hashCode => Object.hash( orderId, packageName, purchaseTime, @@ -100,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. @@ -166,17 +177,19 @@ class PurchaseWrapper { // We can optionally make [PurchaseWrapper] extend or implement [PurchaseHistoryRecordWrapper]. // For now, we keep them separated classes to be consistent with Android's BillingClient implementation. @JsonSerializable() +@immutable class PurchaseHistoryRecordWrapper { /// Creates a [PurchaseHistoryRecordWrapper] with the given record details. @visibleForTesting - PurchaseHistoryRecordWrapper({ + const 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) => @@ -196,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. /// @@ -215,20 +235,23 @@ class PurchaseHistoryRecordWrapper { @override bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchaseHistoryRecordWrapper typedOther = - other as PurchaseHistoryRecordWrapper; - return typedOther.purchaseTime == purchaseTime && - typedOther.purchaseToken == purchaseToken && - typedOther.signature == signature && - typedOther.sku == sku && - typedOther.originalJson == originalJson && - typedOther.developerPayload == developerPayload; + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is PurchaseHistoryRecordWrapper && + other.purchaseTime == purchaseTime && + other.purchaseToken == purchaseToken && + other.signature == signature && + other.sku == sku && + other.originalJson == originalJson && + other.developerPayload == developerPayload; } @override - int get hashCode => hashValues(purchaseTime, purchaseToken, signature, sku, + int get hashCode => Object.hash(purchaseTime, purchaseToken, signature, sku, originalJson, developerPayload); } @@ -241,9 +264,10 @@ class PurchaseHistoryRecordWrapper { /// Wraps [`com.android.billingclient.api.Purchase.PurchasesResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult). @JsonSerializable() @BillingResponseConverter() +@immutable class PurchasesResultWrapper { /// Creates a [PurchasesResultWrapper] with the given purchase result details. - PurchasesResultWrapper( + const PurchasesResultWrapper( {required this.responseCode, required this.billingResult, required this.purchasesList}); @@ -254,16 +278,20 @@ class PurchasesResultWrapper { @override bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchasesResultWrapper typedOther = other as PurchasesResultWrapper; - return typedOther.responseCode == responseCode && - typedOther.purchasesList == purchasesList && - typedOther.billingResult == billingResult; + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is PurchasesResultWrapper && + other.responseCode == responseCode && + other.purchasesList == purchasesList && + other.billingResult == billingResult; } @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; @@ -287,9 +315,10 @@ class PurchasesResultWrapper { /// that contains a detailed description of the status. @JsonSerializable() @BillingResponseConverter() +@immutable class PurchasesHistoryResult { /// Creates a [PurchasesHistoryResult] with the provided history. - PurchasesHistoryResult( + const PurchasesHistoryResult( {required this.billingResult, required this.purchaseHistoryRecordList}); /// Factory for creating a [PurchasesHistoryResult] from a [Map] with the history result details. @@ -298,15 +327,19 @@ class PurchasesHistoryResult { @override bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchasesHistoryResult typedOther = other as PurchasesHistoryResult; - return typedOther.purchaseHistoryRecordList == purchaseHistoryRecordList && - typedOther.billingResult == billingResult; + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is PurchasesHistoryResult && + other.purchaseHistoryRecordList == purchaseHistoryRecordList && + other.billingResult == billingResult; } @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; @@ -323,6 +356,7 @@ class PurchasesHistoryResult { /// Wraps /// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html). /// * See also: [PurchaseWrapper]. +@JsonEnum(alwaysCreate: true) enum PurchaseStateWrapper { /// The state is unspecified. /// @@ -348,3 +382,39 @@ enum PurchaseStateWrapper { @JsonValue(2) pending, } + +/// Serializer for [PurchaseStateWrapper]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@PurchaseStateConverter()`. +class PurchaseStateConverter + implements JsonConverter { + /// Default const constructor. + const PurchaseStateConverter(); + + @override + PurchaseStateWrapper fromJson(int? json) { + if (json == null) { + return PurchaseStateWrapper.unspecified_state; + } + return $enumDecode(_$PurchaseStateWrapperEnumMap, json); + } + + @override + int toJson(PurchaseStateWrapper object) => + _$PurchaseStateWrapperEnumMap[object]!; + + /// Converts the purchase state stored in `object` to a [PurchaseStatus]. + /// + /// [PurchaseStateWrapper.unspecified_state] is mapped to [PurchaseStatus.error]. + PurchaseStatus toPurchaseStatus(PurchaseStateWrapper object) { + switch (object) { + case PurchaseStateWrapper.pending: + return PurchaseStatus.pending; + case PurchaseStateWrapper.purchased: + return PurchaseStatus.purchased; + case PurchaseStateWrapper.unspecified_state: + return PurchaseStatus.error; + } + } +} 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 5607dbdd8cb2..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 @@ -6,108 +6,68 @@ part of 'purchase_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -PurchaseWrapper _$PurchaseWrapperFromJson(Map json) { - return PurchaseWrapper( - orderId: json['orderId'] as String? ?? '', - packageName: json['packageName'] as String? ?? '', - purchaseTime: json['purchaseTime'] as int? ?? 0, - purchaseToken: json['purchaseToken'] as String? ?? '', - signature: json['signature'] as String? ?? '', - sku: json['sku'] as String? ?? '', - isAutoRenewing: json['isAutoRenewing'] as bool, - originalJson: json['originalJson'] as String? ?? '', - developerPayload: json['developerPayload'] as String?, - isAcknowledged: json['isAcknowledged'] as bool? ?? false, - purchaseState: - const PurchaseStateConverter().fromJson(json['purchaseState'] as int?), - obfuscatedAccountId: json['obfuscatedAccountId'] as String?, - obfuscatedProfileId: json['obfuscatedProfileId'] as String?, - ); -} +PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( + orderId: json['orderId'] as String? ?? '', + packageName: json['packageName'] as String? ?? '', + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + 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?, + isAcknowledged: json['isAcknowledged'] as bool? ?? false, + purchaseState: const PurchaseStateConverter() + .fromJson(json['purchaseState'] as int?), + obfuscatedAccountId: json['obfuscatedAccountId'] as String?, + obfuscatedProfileId: json['obfuscatedProfileId'] as String?, + ); -Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => - { - 'orderId': instance.orderId, - 'packageName': instance.packageName, - 'purchaseTime': instance.purchaseTime, - 'purchaseToken': instance.purchaseToken, - 'signature': instance.signature, - 'sku': instance.sku, - 'isAutoRenewing': instance.isAutoRenewing, - 'originalJson': instance.originalJson, - 'developerPayload': instance.developerPayload, - 'isAcknowledged': instance.isAcknowledged, - 'purchaseState': - const PurchaseStateConverter().toJson(instance.purchaseState), - 'obfuscatedAccountId': instance.obfuscatedAccountId, - 'obfuscatedProfileId': instance.obfuscatedProfileId, - }; +PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) => + PurchaseHistoryRecordWrapper( + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + skus: + (json['skus'] as List?)?.map((e) => e as String).toList() ?? + [], + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, + ); -PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { - return PurchaseHistoryRecordWrapper( - purchaseTime: json['purchaseTime'] as int? ?? 0, - purchaseToken: json['purchaseToken'] as String? ?? '', - signature: json['signature'] as String? ?? '', - sku: json['sku'] as String? ?? '', - originalJson: json['originalJson'] as String? ?? '', - developerPayload: json['developerPayload'] as String?, - ); -} +PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) => + PurchasesResultWrapper( + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int?), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchasesList: (json['purchasesList'] as List?) + ?.map((e) => + PurchaseWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], + ); -Map _$PurchaseHistoryRecordWrapperToJson( - PurchaseHistoryRecordWrapper instance) => - { - 'purchaseTime': instance.purchaseTime, - 'purchaseToken': instance.purchaseToken, - 'signature': instance.signature, - 'sku': instance.sku, - 'originalJson': instance.originalJson, - 'developerPayload': instance.developerPayload, - }; +PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) => + PurchasesHistoryResult( + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchaseHistoryRecordList: + (json['purchaseHistoryRecordList'] as List?) + ?.map((e) => PurchaseHistoryRecordWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); -PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { - return PurchasesResultWrapper( - responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int?), - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - purchasesList: (json['purchasesList'] as List?) - ?.map((e) => - PurchaseWrapper.fromJson(Map.from(e as Map))) - .toList() ?? - [], - ); -} - -Map _$PurchasesResultWrapperToJson( - PurchasesResultWrapper instance) => - { - 'billingResult': instance.billingResult, - 'responseCode': - const BillingResponseConverter().toJson(instance.responseCode), - 'purchasesList': instance.purchasesList, - }; - -PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) { - return PurchasesHistoryResult( - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - purchaseHistoryRecordList: - (json['purchaseHistoryRecordList'] as List?) - ?.map((e) => PurchaseHistoryRecordWrapper.fromJson( - Map.from(e as Map))) - .toList() ?? - [], - ); -} - -Map _$PurchasesHistoryResultToJson( - PurchasesHistoryResult instance) => - { - 'billingResult': instance.billingResult, - 'purchaseHistoryRecordList': instance.purchaseHistoryRecordList, - }; +const _$PurchaseStateWrapperEnumMap = { + PurchaseStateWrapper.unspecified_state: 0, + PurchaseStateWrapper.purchased: 1, + PurchaseStateWrapper.pending: 2, +}; 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 9c349badbb04..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,11 +2,10 @@ // 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'; + import 'billing_client_wrapper.dart'; -import 'enum_converters.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to @@ -17,7 +16,7 @@ part 'sku_details_wrapper.g.dart'; /// /// This usually indicates a series underlining code issue in the plugin. @visibleForTesting -const kInvalidBillingResultErrorMessage = +const String kInvalidBillingResultErrorMessage = 'Invalid billing result map from method channel.'; /// Dart wrapper around [`com.android.billingclient.api.SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails). @@ -25,14 +24,17 @@ const kInvalidBillingResultErrorMessage = /// Contains the details of an available product in Google Play Billing. @JsonSerializable() @SkuTypeConverter() +@immutable class SkuDetailsWrapper { /// Creates a [SkuDetailsWrapper] with the given purchase details. @visibleForTesting - SkuDetailsWrapper({ + const SkuDetailsWrapper({ required this.description, required this.freeTrialPeriod, required this.introductoryPrice, - required this.introductoryPriceMicros, + @Deprecated('Use `introductoryPriceAmountMicros` parameter instead') + String introductoryPriceMicros = '', + this.introductoryPriceAmountMicros = 0, required this.introductoryPriceCycles, required this.introductoryPricePeriod, required this.price, @@ -45,7 +47,7 @@ class SkuDetailsWrapper { required this.type, required this.originalPrice, required this.originalPriceAmountMicros, - }); + }) : _introductoryPriceMicros = introductoryPriceMicros; /// Constructs an instance of this from a key value map of data. /// @@ -55,6 +57,8 @@ class SkuDetailsWrapper { factory SkuDetailsWrapper.fromJson(Map map) => _$SkuDetailsWrapperFromJson(map); + final String _introductoryPriceMicros; + /// Textual description of the product. @JsonKey(defaultValue: '') final String description; @@ -67,9 +71,18 @@ class SkuDetailsWrapper { @JsonKey(defaultValue: '') final String introductoryPrice; - /// [introductoryPrice] in micro-units 990000 - @JsonKey(name: 'introductoryPriceAmountMicros', defaultValue: '') - final String introductoryPriceMicros; + /// [introductoryPrice] in micro-units 990000. + /// + /// Returns 0 if the SKU is not a subscription or doesn't have an introductory + /// period. + final int introductoryPriceAmountMicros; + + /// String representation of [introductoryPrice] in micro-units 990000 + @Deprecated('Use `introductoryPriceAmountMicros` instead.') + @JsonKey(ignore: true) + String get introductoryPriceMicros => _introductoryPriceMicros.isEmpty + ? introductoryPriceAmountMicros.toString() + : _introductoryPriceMicros; /// The number of subscription billing periods for which the user will be given the introductory price, such as 3. /// Returns 0 if the SKU is not a subscription or doesn't have an introductory period. @@ -122,7 +135,7 @@ class SkuDetailsWrapper { final int originalPriceAmountMicros; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } @@ -131,7 +144,7 @@ class SkuDetailsWrapper { other.description == description && other.freeTrialPeriod == freeTrialPeriod && other.introductoryPrice == introductoryPrice && - other.introductoryPriceMicros == introductoryPriceMicros && + other.introductoryPriceAmountMicros == introductoryPriceAmountMicros && other.introductoryPriceCycles == introductoryPriceCycles && other.introductoryPricePeriod == introductoryPricePeriod && other.price == price && @@ -146,11 +159,11 @@ class SkuDetailsWrapper { @override int get hashCode { - return hashValues( + return Object.hash( description.hashCode, freeTrialPeriod.hashCode, introductoryPrice.hashCode, - introductoryPriceMicros.hashCode, + introductoryPriceAmountMicros.hashCode, introductoryPriceCycles.hashCode, introductoryPricePeriod.hashCode, price.hashCode, @@ -168,10 +181,11 @@ class SkuDetailsWrapper { /// /// Returned by [BillingClient.querySkuDetails]. @JsonSerializable() +@immutable class SkuDetailsResponseWrapper { /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. @visibleForTesting - SkuDetailsResponseWrapper( + const SkuDetailsResponseWrapper( {required this.billingResult, required this.skuDetailsList}); /// Constructs an instance of this from a key value map of data. @@ -189,7 +203,7 @@ class SkuDetailsResponseWrapper { final List skuDetailsList; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } @@ -200,15 +214,16 @@ 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. @JsonSerializable() @BillingResponseConverter() +@immutable class BillingResultWrapper { /// Constructs the object with [responseCode] and [debugMessage]. - BillingResultWrapper({required this.responseCode, this.debugMessage}); + const BillingResultWrapper({required this.responseCode, this.debugMessage}); /// Constructs an instance of this from a key value map of data. /// @@ -216,7 +231,7 @@ class BillingResultWrapper { /// types of all of the members on this class. factory BillingResultWrapper.fromJson(Map? map) { if (map == null || map.isEmpty) { - return BillingResultWrapper( + return const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage); } @@ -233,7 +248,7 @@ class BillingResultWrapper { final String? debugMessage; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) { return false; } @@ -244,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/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index d21f832a2de6..05eb6bed0035 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -6,81 +6,42 @@ part of 'sku_details_wrapper.dart'; // JsonSerializableGenerator // ************************************************************************** -SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { - return SkuDetailsWrapper( - description: json['description'] as String? ?? '', - freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', - introductoryPrice: json['introductoryPrice'] as String? ?? '', - introductoryPriceMicros: - json['introductoryPriceAmountMicros'] as String? ?? '', - introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, - introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', - price: json['price'] as String? ?? '', - priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, - priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', - priceCurrencySymbol: json['priceCurrencySymbol'] as String? ?? '', - sku: json['sku'] as String? ?? '', - subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', - title: json['title'] as String? ?? '', - type: const SkuTypeConverter().fromJson(json['type'] as String?), - originalPrice: json['originalPrice'] as String? ?? '', - originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, - ); -} - -Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => - { - 'description': instance.description, - 'freeTrialPeriod': instance.freeTrialPeriod, - 'introductoryPrice': instance.introductoryPrice, - 'introductoryPriceAmountMicros': instance.introductoryPriceMicros, - 'introductoryPriceCycles': instance.introductoryPriceCycles, - 'introductoryPricePeriod': instance.introductoryPricePeriod, - 'price': instance.price, - 'priceAmountMicros': instance.priceAmountMicros, - 'priceCurrencyCode': instance.priceCurrencyCode, - 'priceCurrencySymbol': instance.priceCurrencySymbol, - 'sku': instance.sku, - 'subscriptionPeriod': instance.subscriptionPeriod, - 'title': instance.title, - 'type': const SkuTypeConverter().toJson(instance.type), - 'originalPrice': instance.originalPrice, - 'originalPriceAmountMicros': instance.originalPriceAmountMicros, - }; - -SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { - return SkuDetailsResponseWrapper( - billingResult: - BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - skuDetailsList: (json['skuDetailsList'] as List?) - ?.map((e) => - SkuDetailsWrapper.fromJson(Map.from(e as Map))) - .toList() ?? - [], - ); -} - -Map _$SkuDetailsResponseWrapperToJson( - SkuDetailsResponseWrapper instance) => - { - 'billingResult': instance.billingResult, - 'skuDetailsList': instance.skuDetailsList, - }; - -BillingResultWrapper _$BillingResultWrapperFromJson(Map json) { - return BillingResultWrapper( - responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int?), - debugMessage: json['debugMessage'] as String?, - ); -} - -Map _$BillingResultWrapperToJson( - BillingResultWrapper instance) => - { - 'responseCode': - const BillingResponseConverter().toJson(instance.responseCode), - 'debugMessage': instance.debugMessage, - }; +SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => SkuDetailsWrapper( + description: json['description'] as String? ?? '', + freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', + introductoryPrice: json['introductoryPrice'] as String? ?? '', + introductoryPriceAmountMicros: + json['introductoryPriceAmountMicros'] as int? ?? 0, + introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, + introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', + price: json['price'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + priceCurrencySymbol: json['priceCurrencySymbol'] as String? ?? '', + sku: json['sku'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', + title: json['title'] as String? ?? '', + type: const SkuTypeConverter().fromJson(json['type'] as String?), + originalPrice: json['originalPrice'] as String? ?? '', + originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, + ); + +SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) => + SkuDetailsResponseWrapper( + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + skuDetailsList: (json['skuDetailsList'] as List?) + ?.map((e) => SkuDetailsWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); + +BillingResultWrapper _$BillingResultWrapperFromJson(Map json) => + BillingResultWrapper( + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int?), + debugMessage: json['debugMessage'] as String?, + ); 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 f71132a77ef3..14dd69364497 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 @@ -7,7 +7,6 @@ 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'; @@ -40,7 +39,8 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { InAppPurchaseAndroidPlatformAddition(billingClient); _readyFuture = _connect(); - _purchaseUpdatedController = StreamController.broadcast(); + _purchaseUpdatedController = + StreamController>.broadcast(); } /// Registers this class as the default instance of [InAppPurchasePlatform]. @@ -64,7 +64,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { late final BillingClient billingClient; late Future _readyFuture; - static Set _productIdsToConsume = Set(); + static final Set _productIdsToConsume = {}; @override Future isAvailable() async { @@ -78,7 +78,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { List responses; PlatformException? exception; try { - responses = await Future.wait([ + responses = await Future.wait(>[ billingClient.querySkuDetails( skuType: SkuType.inapp, skusList: identifiers.toList()), billingClient.querySkuDetails( @@ -86,30 +86,31 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { ]); } on PlatformException catch (e) { exception = e; - responses = [ + responses = [ // ignore: invalid_use_of_visible_for_testing_member SkuDetailsResponseWrapper( billingResult: BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: []), + skuDetailsList: const []), // ignore: invalid_use_of_visible_for_testing_member SkuDetailsResponseWrapper( billingResult: BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: []) + skuDetailsList: const []) ]; } - List productDetailsList = + final List productDetailsList = responses.expand((SkuDetailsResponseWrapper response) { return response.skuDetailsList; }).map((SkuDetailsWrapper skuDetailWrapper) { return GooglePlayProductDetails.fromSkuDetails(skuDetailWrapper); }).toList(); - Set successIDS = productDetailsList + final Set successIDS = productDetailsList .map((ProductDetails productDetails) => productDetails.id) .toSet(); - List notFoundIDS = identifiers.difference(successIDS).toList(); + final List notFoundIDS = + identifiers.difference(successIDS).toList(); return ProductDetailsResponse( productDetails: productDetailsList, notFoundIDs: notFoundIDS, @@ -130,7 +131,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { changeSubscriptionParam = purchaseParam.changeSubscriptionParam; } - BillingResultWrapper billingResultWrapper = + final BillingResultWrapper billingResultWrapper = await billingClient.launchBillingFlow( sku: purchaseParam.productDetails.id, accountId: purchaseParam.applicationUserName, @@ -158,11 +159,11 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { 'On Android, the `purchase` should always be of type `GooglePlayPurchaseDetails`.', ); - GooglePlayPurchaseDetails googlePurchase = + final GooglePlayPurchaseDetails googlePurchase = purchase as GooglePlayPurchaseDetails; if (googlePurchase.billingClientPurchase.isAcknowledged) { - return BillingResultWrapper(responseCode: BillingResponse.ok); + return const BillingResultWrapper(responseCode: BillingResponse.ok); } if (googlePurchase.verificationData == null) { @@ -180,22 +181,22 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { }) async { List responses; - responses = await Future.wait([ + responses = await Future.wait(>[ billingClient.queryPurchases(SkuType.inapp), billingClient.queryPurchases(SkuType.subs) ]); - Set errorCodeSet = responses + final Set errorCodeSet = responses .where((PurchasesResultWrapper response) => response.responseCode != BillingResponse.ok) .map((PurchasesResultWrapper response) => response.responseCode.toString()) .toSet(); - String errorMessage = + final String errorMessage = errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; - List pastPurchases = + final List pastPurchases = responses.expand((PurchasesResultWrapper response) { return response.purchasesList; }).map((PurchaseWrapper purchaseWrapper) { @@ -229,7 +230,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final BillingResultWrapper billingResult = - await (InAppPurchasePlatformAddition.instance + await (InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition) .consumePurchase(purchaseDetails); final BillingResponse consumedResponse = billingResult.responseCode; @@ -260,17 +261,27 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final List> purchases = resultWrapper.purchasesList.map((PurchaseWrapper purchase) { - return _maybeAutoConsumePurchase( - GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error); + final GooglePlayPurchaseDetails googlePlayPurchaseDetails = + GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error; + if (resultWrapper.responseCode == BillingResponse.userCanceled) { + googlePlayPurchaseDetails.status = PurchaseStatus.canceled; + } + return _maybeAutoConsumePurchase(googlePlayPurchaseDetails); }).toList(); if (purchases.isNotEmpty) { return Future.wait(purchases); } else { - return [ + PurchaseStatus status = PurchaseStatus.error; + if (resultWrapper.responseCode == BillingResponse.userCanceled) { + status = PurchaseStatus.canceled; + } else if (resultWrapper.responseCode == BillingResponse.ok) { + status = PurchaseStatus.purchased; + } + return [ PurchaseDetails( purchaseID: '', productID: '', - status: PurchaseStatus.error, + status: status, transactionDate: null, verificationData: PurchaseVerificationData( localVerificationData: '', 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 11b105aba96c..db53ff4077d2 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 @@ -7,37 +7,39 @@ 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'; /// Contains InApp Purchase features that are only available on PlayStore. class InAppPurchaseAndroidPlatformAddition extends InAppPurchasePlatformAddition { /// Creates a [InAppPurchaseAndroidPlatformAddition] which uses the supplied /// `BillingClient` to provide Android specific features. - InAppPurchaseAndroidPlatformAddition(this._billingClient) { - assert( - _enablePendingPurchase, - 'enablePendingPurchases() must be called when initializing the application and before you access the [InAppPurchase.instance].', - ); - - _billingClient.enablePendingPurchases(); - } + InAppPurchaseAndroidPlatformAddition(this._billingClient); /// Whether pending purchase is enabled. /// + /// **Deprecation warning:** it is no longer required to call + /// [enablePendingPurchases] when initializing your application. From now on + /// this is handled internally and the [enablePendingPurchase] property will + /// always return `true`. + /// + // ignore: deprecated_member_use_from_same_package /// See also [enablePendingPurchases] for more on pending purchases. - static bool get enablePendingPurchase => _enablePendingPurchase; - static bool _enablePendingPurchase = false; + @Deprecated( + '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. /// - /// This method is required to be called when initialize the application. - /// It is to acknowledge your application has been updated to support pending purchases. - /// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending) - /// for more details. - /// Failure to call this method before access [instance] will throw an exception. + /// **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.') static void enablePendingPurchases() { - _enablePendingPurchase = true; + // No-op, until it is time to completely remove this method from the API. } final BillingClient _billingClient; @@ -75,16 +77,16 @@ class InAppPurchaseAndroidPlatformAddition List responses; PlatformException? exception; try { - responses = await Future.wait([ + responses = await Future.wait(>[ _billingClient.queryPurchases(SkuType.inapp), _billingClient.queryPurchases(SkuType.subs) ]); } on PlatformException catch (e) { exception = e; - responses = [ + responses = [ PurchasesResultWrapper( responseCode: BillingResponse.error, - purchasesList: [], + purchasesList: const [], billingResult: BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: e.details.toString(), @@ -92,7 +94,7 @@ class InAppPurchaseAndroidPlatformAddition ), PurchasesResultWrapper( responseCode: BillingResponse.error, - purchasesList: [], + purchasesList: const [], billingResult: BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: e.details.toString(), @@ -101,17 +103,17 @@ class InAppPurchaseAndroidPlatformAddition ]; } - Set errorCodeSet = responses + final Set errorCodeSet = responses .where((PurchasesResultWrapper response) => response.responseCode != BillingResponse.ok) .map((PurchasesResultWrapper response) => response.responseCode.toString()) .toSet(); - String errorMessage = + final String errorMessage = errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; - List pastPurchases = + final List pastPurchases = responses.expand((PurchasesResultWrapper response) { return response.purchasesList; }).map((PurchaseWrapper purchaseWrapper) { 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 59d33fe26223..15ed16c7e2ec 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 @@ -29,10 +29,6 @@ class GooglePlayProductDetails extends ProductDetails { currencySymbol: currencySymbol, ); - /// Points back to the [SkuDetailsWrapper] object that was used to generate - /// this [GooglePlayProductDetails] object. - final SkuDetailsWrapper skuDetails; - /// Generate a [GooglePlayProductDetails] object based on an Android /// [SkuDetailsWrapper] object. factory GooglePlayProductDetails.fromSkuDetails( @@ -43,10 +39,14 @@ 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, ); } + + /// Points back to the [SkuDetailsWrapper] object that was used to generate + /// this [GooglePlayProductDetails] object. + final SkuDetailsWrapper skuDetails; } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart index 53b58bd664fd..42c61a38ddd4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../../billing_client_wrappers.dart'; @@ -26,13 +25,9 @@ class GooglePlayPurchaseDetails extends PurchaseDetails { verificationData: verificationData, status: status, ) { - this.pendingCompletePurchase = !billingClientPurchase.isAcknowledged; + pendingCompletePurchase = !billingClientPurchase.isAcknowledged; } - /// Points back to the [PurchaseWrapper] which was used to generate this - /// [GooglePlayPurchaseDetails] object. - final PurchaseWrapper billingClientPurchase; - /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. factory GooglePlayPurchaseDetails.fromPurchase(PurchaseWrapper purchase) { final GooglePlayPurchaseDetails purchaseDetails = GooglePlayPurchaseDetails( @@ -44,7 +39,8 @@ class GooglePlayPurchaseDetails extends PurchaseDetails { source: kIAPSource), transactionDate: purchase.purchaseTime.toString(), billingClientPurchase: purchase, - status: PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState), + status: const PurchaseStateConverter() + .toPurchaseStatus(purchase.purchaseState), ); if (purchaseDetails.status == PurchaseStatus.error) { @@ -57,4 +53,8 @@ class GooglePlayPurchaseDetails extends PurchaseDetails { return purchaseDetails; } + + /// Points back to the [PurchaseWrapper] which was used to generate this + /// [GooglePlayPurchaseDetails] object. + final PurchaseWrapper billingClientPurchase; } 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 64a40889f375..84bd36a8096b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -1,12 +1,12 @@ 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/master/packages/in_app_purchase/in_app_purchase_android +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.1.4+7 +version: 0.2.3+1 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -20,13 +20,13 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter - in_app_purchase_platform_interface: ^1.1.0 - json_annotation: ^4.0.1 - meta: ^1.3.0 + in_app_purchase_platform_interface: ^1.3.0 + json_annotation: ^4.6.0 dev_dependencies: - build_runner: ^1.11.1 + build_runner: ^2.0.0 flutter_test: sdk: flutter - json_serializable: ^4.1.1 + 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 02ae9ba33564..1e30ce41beda 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 @@ -2,15 +2,14 @@ // 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:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_android/billing_client_wrappers.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase_android/src/channel.dart'; import '../stub_in_app_purchase_platform.dart'; -import 'sku_details_wrapper_test.dart'; import 'purchase_wrapper_test.dart'; +import 'sku_details_wrapper_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -23,7 +22,6 @@ void main() { setUp(() { billingClient = BillingClient((PurchasesResultWrapper _) {}); - billingClient.enablePendingPurchases(); stubPlatform.reset(); }); @@ -42,7 +40,7 @@ void main() { // Make sure that the enum values are supported and that the converter call // does not fail test('response states', () async { - BillingResponseConverter converter = BillingResponseConverter(); + const BillingResponseConverter converter = BillingResponseConverter(); converter.fromJson(-3); converter.fromJson(-2); converter.fromJson(-1); @@ -58,20 +56,20 @@ void main() { }); group('startConnection', () { - final String methodName = + const String methodName = 'BillingClient#startConnection(BillingClientStateListener)'; test('returns BillingResultWrapper', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; + const BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse( name: methodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, ); - BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); expect( await billingClient.startConnection( @@ -81,20 +79,17 @@ void main() { test('passes handle to onBillingServiceDisconnected', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; + const BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse( name: methodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, ); await billingClient.startConnection(onBillingServiceDisconnected: () {}); final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect( - call.arguments, - equals( - {'handle': 0, 'enablePendingPurchases': true})); + expect(call.arguments, equals({'handle': 0})); }); test('handles method channel returning null', () async { @@ -106,14 +101,14 @@ void main() { expect( await billingClient.startConnection( onBillingServiceDisconnected: () {}), - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); }); }); test('endConnection', () async { - final String endConnectionName = 'BillingClient#endConnection()'; + const String endConnectionName = 'BillingClient#endConnection()'; expect(stubPlatform.countPreviousCalls(endConnectionName), equals(0)); stubPlatform.addResponse(name: endConnectionName, value: null); await billingClient.endConnection(); @@ -121,15 +116,15 @@ void main() { }); group('querySkuDetails', () { - final String queryMethodName = + const String queryMethodName = 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; test('handles empty skuDetails', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; + const BillingResponse responseCode = BillingResponse.developerError; stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, 'skuDetailsList': >[] @@ -139,7 +134,7 @@ void main() { .querySkuDetails( skuType: SkuType.inapp, skusList: ['invalid']); - BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); expect(response.billingResult, equals(billingResult)); expect(response.skuDetailsList, isEmpty); @@ -147,10 +142,10 @@ void main() { test('returns SkuDetailsResponseWrapper', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; + const BillingResponse responseCode = BillingResponse.ok; stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] @@ -160,7 +155,7 @@ void main() { .querySkuDetails( skuType: SkuType.inapp, skusList: ['invalid']); - BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); expect(response.billingResult, equals(billingResult)); expect(response.skuDetailsList, contains(dummySkuDetails)); @@ -173,7 +168,7 @@ void main() { .querySkuDetails( skuType: SkuType.inapp, skusList: ['invalid']); - BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage); expect(response.billingResult, equals(billingResult)); @@ -182,21 +177,21 @@ void main() { }); group('launchBillingFlow', () { - final String launchMethodName = + const String launchMethodName = 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; test('serializes and deserializes data', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; - final String profileId = "hashedProfileId"; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String profileId = 'hashedProfileId'; expect( await billingClient.launchBillingFlow( @@ -204,8 +199,9 @@ void main() { accountId: accountId, obfuscatedProfileId: profileId), equals(expectedBillingResult)); - Map arguments = - stubPlatform.previousCallMatching(launchMethodName).arguments; + final Map arguments = stubPlatform + .previousCallMatching(launchMethodName) + .arguments as Map; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], equals(accountId)); expect(arguments['obfuscatedProfileId'], equals(profileId)); @@ -215,16 +211,16 @@ void main() { 'Change subscription throws assertion error `oldSku` and `purchaseToken` has different nullability', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = 'hashedAccountId'; - final String profileId = 'hashedProfileId'; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String profileId = 'hashedProfileId'; expect( billingClient.launchBillingFlow( @@ -249,16 +245,16 @@ void main() { 'serializes and deserializes data on change subscription without proration', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = 'hashedAccountId'; - final String profileId = 'hashedProfileId'; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String profileId = 'hashedProfileId'; expect( await billingClient.launchBillingFlow( @@ -268,8 +264,9 @@ void main() { oldSku: dummyOldPurchase.sku, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); - Map arguments = - stubPlatform.previousCallMatching(launchMethodName).arguments; + 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)); @@ -282,17 +279,18 @@ void main() { 'serializes and deserializes data on change subscription with proration', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = 'hashedAccountId'; - final String profileId = 'hashedProfileId'; - final prorationMode = ProrationMode.immediateAndChargeProratedPrice; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String profileId = 'hashedProfileId'; + const ProrationMode prorationMode = + ProrationMode.immediateAndChargeProratedPrice; expect( await billingClient.launchBillingFlow( @@ -303,8 +301,9 @@ void main() { prorationMode: prorationMode, purchaseToken: dummyOldPurchase.purchaseToken), equals(expectedBillingResult)); - Map arguments = - stubPlatform.previousCallMatching(launchMethodName).arguments; + 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)); @@ -312,24 +311,25 @@ void main() { expect( arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); expect(arguments['prorationMode'], - ProrationModeConverter().toJson(prorationMode)); + const ProrationModeConverter().toJson(prorationMode)); }); test('handles null accountId', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; + const SkuDetailsWrapper skuDetails = dummySkuDetails; expect(await billingClient.launchBillingFlow(sku: skuDetails.sku), equals(expectedBillingResult)); - Map arguments = - stubPlatform.previousCallMatching(launchMethodName).arguments; + final Map arguments = stubPlatform + .previousCallMatching(launchMethodName) + .arguments as Map; expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], isNull); }); @@ -339,10 +339,10 @@ void main() { name: launchMethodName, value: null, ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; + const SkuDetailsWrapper skuDetails = dummySkuDetails; expect( await billingClient.launchBillingFlow(sku: skuDetails.sku), - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); }); @@ -353,17 +353,17 @@ void main() { 'BillingClient#queryPurchases(String)'; test('serializes and deserializes data', () async { - final BillingResponse expectedCode = BillingResponse.ok; + const BillingResponse expectedCode = BillingResponse.ok; final List expectedList = [ dummyPurchase ]; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform .addResponse(name: queryPurchasesMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(expectedCode), + 'responseCode': const BillingResponseConverter().toJson(expectedCode), 'purchasesList': expectedList .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) .toList(), @@ -378,15 +378,15 @@ void main() { }); test('handles empty purchases', () async { - final BillingResponse expectedCode = BillingResponse.userCanceled; + const BillingResponse expectedCode = BillingResponse.userCanceled; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform .addResponse(name: queryPurchasesMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(expectedCode), - 'purchasesList': [], + 'responseCode': const BillingResponseConverter().toJson(expectedCode), + 'purchasesList': [], }); final PurchasesResultWrapper response = @@ -407,7 +407,7 @@ void main() { expect( response.billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); expect(response.responseCode, BillingResponse.error); @@ -420,13 +420,13 @@ void main() { 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)'; test('serializes and deserializes data', () async { - final BillingResponse expectedCode = BillingResponse.ok; + const BillingResponse expectedCode = BillingResponse.ok; final List expectedList = [ dummyPurchaseHistoryRecord, ]; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: queryPurchaseHistoryMethodName, @@ -445,14 +445,16 @@ void main() { }); test('handles empty purchases', () async { - final BillingResponse expectedCode = BillingResponse.userCanceled; + const BillingResponse expectedCode = BillingResponse.userCanceled; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryPurchaseHistoryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchaseHistoryRecordList': [], - }); + stubPlatform.addResponse( + name: queryPurchaseHistoryMethodName, + value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchaseHistoryRecordList': [], + }); final PurchasesHistoryResult response = await billingClient.queryPurchaseHistory(SkuType.inapp); @@ -471,7 +473,7 @@ void main() { expect( response.billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); expect(response.purchaseHistoryRecordList, isEmpty); @@ -482,9 +484,9 @@ void main() { const String consumeMethodName = 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; test('consume purchase async success', () async { - final BillingResponse expectedCode = BillingResponse.ok; + const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, @@ -506,7 +508,7 @@ void main() { expect( billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); }); @@ -516,9 +518,9 @@ void main() { const String acknowledgeMethodName = 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('acknowledge purchase success', () async { - final BillingResponse expectedCode = BillingResponse.ok; + const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: acknowledgeMethodName, @@ -539,7 +541,7 @@ void main() { expect( billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); }); @@ -553,7 +555,8 @@ void main() { stubPlatform.addResponse( name: isFeatureSupportedMethodName, value: false, - additionalStepBeforeReturn: (value) => arguments = value, + additionalStepBeforeReturn: (dynamic value) => + arguments = value as Map, ); final bool isSupported = await billingClient .isFeatureSupported(BillingClientFeature.subscriptions); @@ -566,7 +569,8 @@ void main() { stubPlatform.addResponse( name: isFeatureSupportedMethodName, value: true, - additionalStepBeforeReturn: (value) => arguments = value, + additionalStepBeforeReturn: (dynamic value) => + arguments = value as Map, ); final bool isSupported = await billingClient .isFeatureSupported(BillingClientFeature.subscriptions); @@ -579,7 +583,8 @@ void main() { const String launchPriceChangeConfirmationFlowMethodName = 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)'; - final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper( + const BillingResultWrapper expectedBillingResultPriceChangeConfirmation = + BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message', ); 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 70b9fcad4da7..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 @@ -4,15 +4,14 @@ 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/billing_client_wrappers/enum_converters.dart'; import 'package:test/test.dart'; -final PurchaseWrapper dummyPurchase = PurchaseWrapper( +const PurchaseWrapper dummyPurchase = PurchaseWrapper( orderId: 'orderId', packageName: 'packageName', purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -23,12 +22,12 @@ final PurchaseWrapper dummyPurchase = PurchaseWrapper( obfuscatedProfileId: 'Profile103', ); -final PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( +const PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( orderId: 'orderId', packageName: 'packageName', purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', isAutoRenewing: false, originalJson: '', @@ -37,22 +36,22 @@ final PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( purchaseState: PurchaseStateWrapper.purchased, ); -final PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = +const PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = PurchaseHistoryRecordWrapper( purchaseTime: 0, signature: 'signature', - sku: 'sku', + skus: ['sku'], purchaseToken: 'purchaseToken', originalJson: '', developerPayload: 'dummy payload', ); -final PurchaseWrapper dummyOldPurchase = PurchaseWrapper( +const PurchaseWrapper dummyOldPurchase = PurchaseWrapper( orderId: 'oldOrderId', packageName: 'oldPackageName', purchaseTime: 0, signature: 'oldSignature', - sku: 'oldSku', + skus: ['oldSku'], purchaseToken: 'oldPurchaseToken', isAutoRenewing: false, originalJson: '', @@ -64,7 +63,7 @@ final PurchaseWrapper dummyOldPurchase = PurchaseWrapper( void main() { group('PurchaseWrapper', () { test('converts from map', () { - final PurchaseWrapper expected = dummyPurchase; + const PurchaseWrapper expected = dummyPurchase; final PurchaseWrapper parsed = PurchaseWrapper.fromJson(buildPurchaseMap(expected)); @@ -110,7 +109,7 @@ void main() { group('PurchaseHistoryRecordWrapper', () { test('converts from map', () { - final PurchaseHistoryRecordWrapper expected = dummyPurchaseHistoryRecord; + const PurchaseHistoryRecordWrapper expected = dummyPurchaseHistoryRecord; final PurchaseHistoryRecordWrapper parsed = PurchaseHistoryRecordWrapper.fromJson( buildPurchaseHistoryRecordMap(expected)); @@ -121,13 +120,13 @@ void main() { group('PurchasesResultWrapper', () { test('parsed from map', () { - final BillingResponse responseCode = BillingResponse.ok; + const BillingResponse responseCode = BillingResponse.ok; final List purchases = [ dummyPurchase, dummyPurchase ]; const String debugMessage = 'dummy Message'; - final BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); final PurchasesResultWrapper expected = PurchasesResultWrapper( billingResult: billingResult, @@ -136,7 +135,7 @@ void main() { final PurchasesResultWrapper parsed = PurchasesResultWrapper.fromJson({ 'billingResult': buildBillingResultMap(billingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'purchasesList': >[ buildPurchaseMap(dummyPurchase), buildPurchaseMap(dummyPurchase) @@ -149,10 +148,10 @@ void main() { test('parsed from empty map', () { final PurchasesResultWrapper parsed = - PurchasesResultWrapper.fromJson({}); + PurchasesResultWrapper.fromJson(const {}); expect( parsed.billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); expect(parsed.responseCode, BillingResponse.error); @@ -162,14 +161,14 @@ void main() { group('PurchasesHistoryResult', () { test('parsed from map', () { - final BillingResponse responseCode = BillingResponse.ok; + const BillingResponse responseCode = BillingResponse.ok; final List purchaseHistoryRecordList = [ dummyPurchaseHistoryRecord, dummyPurchaseHistoryRecord ]; const String debugMessage = 'dummy Message'; - final BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); final PurchasesHistoryResult expected = PurchasesHistoryResult( billingResult: billingResult, @@ -189,10 +188,10 @@ void main() { test('parsed from empty map', () { final PurchasesHistoryResult parsed = - PurchasesHistoryResult.fromJson({}); + PurchasesHistoryResult.fromJson(const {}); expect( parsed.billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); expect(parsed.purchaseHistoryRecordList, isEmpty); @@ -206,12 +205,13 @@ 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, 'developerPayload': original.developerPayload, - 'purchaseState': PurchaseStateConverter().toJson(original.purchaseState), + 'purchaseState': + const PurchaseStateConverter().toJson(original.purchaseState), 'isAcknowledged': original.isAcknowledged, 'obfuscatedAccountId': original.obfuscatedAccountId, 'obfuscatedProfileId': original.obfuscatedProfileId, @@ -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, @@ -232,7 +232,8 @@ Map buildPurchaseHistoryRecordMap( Map buildBillingResultMap(BillingResultWrapper original) { return { - 'responseCode': BillingResponseConverter().toJson(original.responseCode), + 'responseCode': + const BillingResponseConverter().toJson(original.responseCode), 'debugMessage': original.debugMessage, }; } diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart new file mode 100644 index 000000000000..f27ea02209c4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.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. + +// TODO(mvanbeusekom): Remove this file when the deprecated +// `SkuDetailsWrapper.introductoryPriceMicros` field is +// removed. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +void main() { + test( + 'Deprecated `introductoryPriceMicros` field reflects parameter from constructor', + () { + const SkuDetailsWrapper skuDetails = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + // ignore: deprecated_member_use_from_same_package + introductoryPriceMicros: '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(skuDetails, isNotNull); + expect(skuDetails.introductoryPriceAmountMicros, 0); + // ignore: deprecated_member_use_from_same_package + expect(skuDetails.introductoryPriceMicros, '990000'); + }); + + test( + '`introductoryPriceAmoutMicros` constructor parameter is reflected by deprecated `introductoryPriceMicros` and `introductoryPriceAmountMicros` fields', + () { + const SkuDetailsWrapper skuDetails = 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(skuDetails, isNotNull); + expect(skuDetails.introductoryPriceAmountMicros, 990000); + // ignore: deprecated_member_use_from_same_package + expect(skuDetails.introductoryPriceMicros, '990000'); + }); +} 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 62d9104f3738..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 @@ -2,16 +2,15 @@ // 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_android/src/types/google_play_product_details.dart'; import 'package:test/test.dart'; -import 'package:in_app_purchase_android/billing_client_wrappers.dart'; -import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart'; -final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( +const SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( description: 'description', freeTrialPeriod: 'freeTrialPeriod', introductoryPrice: 'introductoryPrice', - introductoryPriceMicros: 'introductoryPriceMicros', + introductoryPriceAmountMicros: 990000, introductoryPriceCycles: 1, introductoryPricePeriod: 'introductoryPricePeriod', price: 'price', @@ -29,7 +28,7 @@ final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( void main() { group('SkuDetailsWrapper', () { test('converts from map', () { - final SkuDetailsWrapper expected = dummySkuDetails; + const SkuDetailsWrapper expected = dummySkuDetails; final SkuDetailsWrapper parsed = SkuDetailsWrapper.fromJson(buildSkuMap(expected)); @@ -39,13 +38,13 @@ void main() { group('SkuDetailsResponseWrapper', () { test('parsed from map', () { - final BillingResponse responseCode = BillingResponse.ok; + const BillingResponse responseCode = BillingResponse.ok; const String debugMessage = 'dummy message'; final List skusDetails = [ dummySkuDetails, dummySkuDetails ]; - BillingResultWrapper result = BillingResultWrapper( + const BillingResultWrapper result = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( billingResult: result, skuDetailsList: skusDetails); @@ -53,7 +52,7 @@ void main() { final SkuDetailsResponseWrapper parsed = SkuDetailsResponseWrapper.fromJson({ 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, 'skuDetailsList': >[ @@ -79,10 +78,10 @@ void main() { }); test('handles empty list of skuDetails', () { - final BillingResponse responseCode = BillingResponse.error; + const BillingResponse responseCode = BillingResponse.error; const String debugMessage = 'dummy message'; final List skusDetails = []; - BillingResultWrapper billingResult = BillingResultWrapper( + const BillingResultWrapper billingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( billingResult: billingResult, skuDetailsList: skusDetails); @@ -90,10 +89,10 @@ void main() { final SkuDetailsResponseWrapper parsed = SkuDetailsResponseWrapper.fromJson({ 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'debugMessage': debugMessage, }, - 'skuDetailsList': >[] + 'skuDetailsList': const >[] }); expect(parsed.billingResult, equals(expected.billingResult)); @@ -102,10 +101,10 @@ void main() { test('fromJson creates an object with default values', () { final SkuDetailsResponseWrapper skuDetails = - SkuDetailsResponseWrapper.fromJson({}); + SkuDetailsResponseWrapper.fromJson(const {}); expect( skuDetails.billingResult, - equals(BillingResultWrapper( + equals(const BillingResultWrapper( responseCode: BillingResponse.error, debugMessage: kInvalidBillingResultErrorMessage))); expect(skuDetails.skuDetailsList, isEmpty); @@ -115,7 +114,7 @@ void main() { group('BillingResultWrapper', () { test('fromJson on empty map creates an object with default values', () { final BillingResultWrapper billingResult = - BillingResultWrapper.fromJson({}); + BillingResultWrapper.fromJson(const {}); expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); expect(billingResult.responseCode, BillingResponse.error); }); @@ -126,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); + }); }); } @@ -134,7 +187,7 @@ Map buildSkuMap(SkuDetailsWrapper original) { 'description': original.description, 'freeTrialPeriod': original.freeTrialPeriod, 'introductoryPrice': original.introductoryPrice, - 'introductoryPriceAmountMicros': original.introductoryPriceMicros, + 'introductoryPriceAmountMicros': original.introductoryPriceAmountMicros, 'introductoryPriceCycles': original.introductoryPriceCycles, 'introductoryPricePeriod': original.introductoryPricePeriod, 'price': original.price, 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 a478cabac89b..c87d0e39f0c2 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 @@ -7,9 +7,7 @@ import 'package:flutter/widgets.dart' as widgets; 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/billing_client_wrappers/enum_converters.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'; @@ -30,11 +28,9 @@ void main() { setUp(() { widgets.WidgetsFlutterBinding.ensureInitialized(); - InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: startConnectionCall, @@ -48,9 +44,9 @@ void main() { const String consumeMethodName = 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; test('consume purchase async success', () async { - final BillingResponse expectedCode = BillingResponse.ok; + const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, @@ -69,14 +65,14 @@ void main() { const String queryMethodName = 'BillingClient#queryPurchases(String)'; test('handles error', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.developerError; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform .addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'purchasesList': >[] }); final QueryPurchaseDetailsResponse response = @@ -90,14 +86,14 @@ void main() { test('returns SkuDetailsResponseWrapper', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform .addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'purchasesList': >[ buildPurchaseMap(dummyPurchase), ] @@ -114,21 +110,22 @@ void main() { test('should store platform exception in the response', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.developerError; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': + const BillingResponseConverter().toJson(responseCode), 'billingResult': buildBillingResultMap(expectedBillingResult), 'purchasesList': >[] }, - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { throw PlatformException( code: 'error_code', message: 'error_message', - details: {'info': 'error_info'}, + details: {'info': 'error_info'}, ); }); final QueryPurchaseDetailsResponse response = @@ -137,7 +134,8 @@ void main() { expect(response.error, isNotNull); expect(response.error!.code, 'error_code'); expect(response.error!.message, 'error_message'); - expect(response.error!.details, {'info': 'error_info'}); + expect( + response.error!.details, {'info': 'error_info'}); }); }); }); @@ -150,7 +148,8 @@ void main() { stubPlatform.addResponse( name: isFeatureSupportedMethodName, value: false, - additionalStepBeforeReturn: (value) => arguments = value, + additionalStepBeforeReturn: (dynamic value) => + arguments = value as Map, ); final bool isSupported = await iapAndroidPlatformAddition .isFeatureSupported(BillingClientFeature.subscriptions); @@ -163,7 +162,8 @@ void main() { stubPlatform.addResponse( name: isFeatureSupportedMethodName, value: true, - additionalStepBeforeReturn: (value) => arguments = value, + additionalStepBeforeReturn: (dynamic value) => + arguments = value as Map, ); final bool isSupported = await iapAndroidPlatformAddition .isFeatureSupported(BillingClientFeature.subscriptions); @@ -175,9 +175,10 @@ void main() { group('launchPriceChangeConfirmationFlow', () { const String launchPriceChangeConfirmationFlowMethodName = 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)'; - const dummySku = 'sku'; + const String dummySku = 'sku'; - final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper( + const BillingResultWrapper expectedBillingResultPriceChangeConfirmation = + BillingResultWrapper( responseCode: BillingResponse.ok, debugMessage: 'dummy message', ); 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 52ec08bea07a..4f90dccf94f4 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 @@ -9,9 +9,7 @@ import 'package:flutter/widgets.dart' as widgets; 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/billing_client_wrappers/enum_converters.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 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'billing_client_wrappers/purchase_wrapper_test.dart'; @@ -35,15 +33,14 @@ void main() { widgets.WidgetsFlutterBinding.ensureInitialized(); const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: startConnectionCall, value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall, value: null); - InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); InAppPurchaseAndroidPlatform.registerPlatform(); iapAndroidPlatform = InAppPurchasePlatform.instance as InAppPurchaseAndroidPlatform; @@ -73,28 +70,28 @@ void main() { }); group('querySkuDetails', () { - final String queryMethodName = + const String queryMethodName = 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; test('handles empty skuDetails', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': [], + 'skuDetailsList': >[], }); final ProductDetailsResponse response = - await iapAndroidPlatform.queryProductDetails([''].toSet()); + await iapAndroidPlatform.queryProductDetails({''}); expect(response.productDetails, isEmpty); }); test('should get correct product details', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), @@ -102,8 +99,8 @@ void main() { }); // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead // of 1. - final ProductDetailsResponse response = await iapAndroidPlatform - .queryProductDetails(['valid'].toSet()); + final ProductDetailsResponse response = + await iapAndroidPlatform.queryProductDetails({'valid'}); expect(response.productDetails.first.title, dummySkuDetails.title); expect(response.productDetails.first.description, dummySkuDetails.description); @@ -113,8 +110,8 @@ void main() { test('should get the correct notFoundIDs', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), @@ -122,41 +119,42 @@ void main() { }); // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead // of 1. - final ProductDetailsResponse response = await iapAndroidPlatform - .queryProductDetails(['invalid'].toSet()); + final ProductDetailsResponse response = + await iapAndroidPlatform.queryProductDetails({'invalid'}); expect(response.notFoundIDs.first, 'invalid'); }); test( 'should have error stored in the response when platform exception is thrown', () async { - final BillingResponse responseCode = BillingResponse.ok; + const BillingResponse responseCode = BillingResponse.ok; stubPlatform.addResponse( name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': + const BillingResponseConverter().toJson(responseCode), 'skuDetailsList': >[ buildSkuMap(dummySkuDetails) ] }, - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { throw PlatformException( code: 'error_code', message: 'error_message', - details: {'info': 'error_info'}, + details: {'info': 'error_info'}, ); }); // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead // of 1. - final ProductDetailsResponse response = await iapAndroidPlatform - .queryProductDetails(['invalid'].toSet()); - expect(response.notFoundIDs, ['invalid']); + final ProductDetailsResponse response = + await iapAndroidPlatform.queryProductDetails({'invalid'}); + expect(response.notFoundIDs, ['invalid']); expect(response.productDetails, isEmpty); expect(response.error, isNotNull); expect(response.error!.source, kIAPSource); expect(response.error!.code, 'error_code'); expect(response.error!.message, 'error_message'); - expect(response.error!.details, {'info': 'error_info'}); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -164,13 +162,13 @@ void main() { const String queryMethodName = 'BillingClient#queryPurchases(String)'; test('handles error', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.developerError; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'purchasesList': >[] }); @@ -178,9 +176,12 @@ void main() { iapAndroidPlatform.restorePurchases(), throwsA( isA() - .having((e) => e.source, 'source', kIAPSource) - .having((e) => e.code, 'code', kRestoredPurchaseErrorCode) - .having((e) => e.message, 'message', responseCode.toString()), + .having( + (InAppPurchaseException e) => e.source, 'source', kIAPSource) + .having((InAppPurchaseException e) => e.code, 'code', + kRestoredPurchaseErrorCode) + .having((InAppPurchaseException e) => e.message, 'message', + responseCode.toString()), ), ); }); @@ -188,21 +189,22 @@ void main() { test('should store platform exception in the response', () async { const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.developerError; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse( name: queryMethodName, value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': + const BillingResponseConverter().toJson(responseCode), 'billingResult': buildBillingResultMap(expectedBillingResult), 'purchasesList': >[] }, - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { throw PlatformException( code: 'error_code', message: 'error_message', - details: {'info': 'error_info'}, + details: {'info': 'error_info'}, ); }); @@ -210,19 +212,23 @@ void main() { iapAndroidPlatform.restorePurchases(), throwsA( isA() - .having((e) => e.code, 'code', 'error_code') - .having((e) => e.message, 'message', 'error_message') - .having((e) => e.details, 'details', {'info': 'error_info'}), + .having((PlatformException e) => e.code, 'code', 'error_code') + .having((PlatformException e) => e.message, 'message', + 'error_message') + .having((PlatformException e) => e.details, 'details', + {'info': 'error_info'}), ), ); }); test('returns SkuDetailsResponseWrapper', () async { - Completer completer = Completer(); - Stream> stream = iapAndroidPlatform.purchaseStream; + final Completer> completer = + Completer>(); + final Stream> stream = + iapAndroidPlatform.purchaseStream; - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { if (purchaseDetailsList.first.status == PurchaseStatus.restored) { completer.complete(purchaseDetailsList); subscription.cancel(); @@ -230,13 +236,13 @@ void main() { }); const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse responseCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: responseCode, debugMessage: debugMessage); stubPlatform.addResponse(name: queryMethodName, value: { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), + 'responseCode': const BillingResponseConverter().toJson(responseCode), 'purchasesList': >[ buildPurchaseMap(dummyPurchase), ] @@ -249,8 +255,8 @@ void main() { final List restoredPurchases = await completer.future; expect(restoredPurchases.length, 2); - restoredPurchases.forEach((element) { - GooglePlayPurchaseDetails purchase = + for (final PurchaseDetails element in restoredPurchases) { + final GooglePlayPurchaseDetails purchase = element as GooglePlayPurchaseDetails; expect(purchase.productID, dummyPurchase.sku); @@ -263,40 +269,41 @@ void main() { expect(purchase.transactionDate, dummyPurchase.purchaseTime.toString()); expect(purchase.billingClientPurchase, dummyPurchase); expect(purchase.status, PurchaseStatus.restored); - }); + } }); }); group('make payment', () { - final String launchMethodName = + const String launchMethodName = 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)'; const String consumeMethodName = 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; test('buy non consumable, serializes and deserializes data', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, - 'packageName': "package", + 'packageName': 'package', 'purchaseTime': 1231231231, - 'purchaseToken': "token", + 'purchaseToken': 'token', 'signature': 'sign', 'originalJson': 'json', 'developerPayload': 'dummy payload', @@ -307,10 +314,11 @@ void main() { }); iapAndroidPlatform.billingClient.callHandler(call); }); - Completer completer = Completer(); + final Completer completer = Completer(); PurchaseDetails purchaseDetails; - Stream purchaseStream = iapAndroidPlatform.purchaseStream; - late StreamSubscription subscription; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -322,7 +330,7 @@ void main() { final bool launchResult = await iapAndroidPlatform.buyNonConsumable( purchaseParam: purchaseParam); - PurchaseDetails result = await completer.future; + final PurchaseDetails result = await completer.future; expect(launchResult, isTrue); expect(result.purchaseID, 'orderID1'); expect(result.status, PurchaseStatus.purchased); @@ -330,29 +338,31 @@ void main() { }); test('handles an error with an empty purchases list', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.error; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.error; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(sentCode), - 'purchasesList': [] + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': const [] }); iapAndroidPlatform.billingClient.callHandler(call); }); - Completer completer = Completer(); + final Completer completer = Completer(); PurchaseDetails purchaseDetails; - Stream purchaseStream = iapAndroidPlatform.purchaseStream; - late StreamSubscription subscription; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -362,7 +372,7 @@ void main() { productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), applicationUserName: accountId); await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam); - PurchaseDetails result = await completer.future; + final PurchaseDetails result = await completer.future; expect(result.error, isNotNull); expect(result.error!.source, kIAPSource); @@ -372,29 +382,30 @@ void main() { test('buy consumable with auto consume, serializes and deserializes data', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, - 'packageName': "package", + 'packageName': 'package', 'purchaseTime': 1231231231, - 'purchaseToken': "token", + 'purchaseToken': 'token', 'signature': 'sign', 'originalJson': 'json', 'developerPayload': 'dummy payload', @@ -405,24 +416,25 @@ void main() { }); iapAndroidPlatform.billingClient.callHandler(call); }); - Completer consumeCompleter = Completer(); + final Completer consumeCompleter = Completer(); // adding call back for consume purchase - final BillingResponse expectedCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResultForConsume = + const BillingResponse expectedCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - String purchaseToken = args['purchaseToken']; - consumeCompleter.complete((purchaseToken)); + final String purchaseToken = args['purchaseToken'] as String; + consumeCompleter.complete(purchaseToken); }); - Completer completer = Completer(); + final Completer completer = Completer(); PurchaseDetails purchaseDetails; - Stream purchaseStream = iapAndroidPlatform.purchaseStream; - late StreamSubscription subscription; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -435,7 +447,8 @@ void main() { await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); // Verify that the result has succeeded - GooglePlayPurchaseDetails result = await completer.future; + final GooglePlayPurchaseDetails result = + await completer.future as GooglePlayPurchaseDetails; expect(launchResult, isTrue); expect(result.billingClientPurchase, isNotNull); expect(result.billingClientPurchase.purchaseToken, @@ -447,8 +460,8 @@ void main() { test('buyNonConsumable propagates failures to launch the billing flow', () async { const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.error; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.error; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, @@ -466,8 +479,8 @@ void main() { test('buyConsumable propagates failures to launch the billing flow', () async { const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.developerError; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, @@ -484,28 +497,29 @@ void main() { }); test('adds consumption failures to PurchaseDetails objects', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, - 'packageName': "package", + 'packageName': 'package', 'purchaseTime': 1231231231, - 'purchaseToken': "token", + 'purchaseToken': 'token', 'signature': 'sign', 'originalJson': 'json', 'developerPayload': 'dummy payload', @@ -516,24 +530,25 @@ void main() { }); iapAndroidPlatform.billingClient.callHandler(call); }); - Completer consumeCompleter = Completer(); + final Completer consumeCompleter = Completer(); // adding call back for consume purchase - final BillingResponse expectedCode = BillingResponse.error; - final BillingResultWrapper expectedBillingResultForConsume = + const BillingResponse expectedCode = BillingResponse.error; + const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - String purchaseToken = args['purchaseToken']; + final String purchaseToken = args['purchaseToken'] as String; consumeCompleter.complete(purchaseToken); }); - Completer completer = Completer(); + final Completer completer = Completer(); PurchaseDetails purchaseDetails; - Stream purchaseStream = iapAndroidPlatform.purchaseStream; - late StreamSubscription subscription; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -545,7 +560,8 @@ void main() { await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); // Verify that the result has an error for the failed consumption - GooglePlayPurchaseDetails result = await completer.future; + final GooglePlayPurchaseDetails result = + await completer.future as GooglePlayPurchaseDetails; expect(result.billingClientPurchase, isNotNull); expect(result.billingClientPurchase.purchaseToken, await consumeCompleter.future); @@ -557,29 +573,30 @@ void main() { test( 'buy consumable without auto consume, consume api should not receive calls', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResponse sentCode = BillingResponse.developerError; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: sentCode, debugMessage: debugMessage); stubPlatform.addResponse( name: launchMethodName, value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { + additionalStepBeforeReturn: (dynamic _) { // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(sentCode), - 'purchasesList': [ - { + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { 'orderId': 'orderID1', - 'sku': skuDetails.sku, + 'skus': [skuDetails.sku], 'isAutoRenewing': false, - 'packageName': "package", + 'packageName': 'package', 'purchaseTime': 1231231231, - 'purchaseToken': "token", + 'purchaseToken': 'token', 'signature': 'sign', 'originalJson': 'json', 'developerPayload': 'dummy payload', @@ -590,22 +607,23 @@ void main() { }); iapAndroidPlatform.billingClient.callHandler(call); }); - Completer consumeCompleter = Completer(); + final Completer consumeCompleter = Completer(); // adding call back for consume purchase - final BillingResponse expectedCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResultForConsume = + const BillingResponse expectedCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResultForConsume = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - String purchaseToken = args['purchaseToken']; - consumeCompleter.complete((purchaseToken)); + final String purchaseToken = args['purchaseToken'] as String; + consumeCompleter.complete(purchaseToken); }); - Stream purchaseStream = iapAndroidPlatform.purchaseStream; - late StreamSubscription subscription; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; subscription = purchaseStream.listen((_) { consumeCompleter.complete(null); subscription.cancel(); @@ -617,23 +635,142 @@ void main() { purchaseParam: purchaseParam, autoConsume: false); expect(null, await consumeCompleter.future); }); + + test( + 'should get canceled purchase status when response code is BillingResponse.userCanceled', + () async { + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String debugMessage = 'dummy message'; + const BillingResponse sentCode = BillingResponse.userCanceled; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (dynamic _) { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': [ + { + 'orderId': 'orderID1', + 'sku': skuDetails.sku, + 'isAutoRenewing': false, + 'packageName': 'package', + 'purchaseTime': 1231231231, + 'purchaseToken': 'token', + 'signature': 'sign', + 'originalJson': 'json', + 'developerPayload': 'dummy payload', + 'isAcknowledged': true, + 'purchaseState': 1, + } + ] + }); + iapAndroidPlatform.billingClient.callHandler(call); + }); + final Completer consumeCompleter = Completer(); + // adding call back for consume purchase + const BillingResponse expectedCode = BillingResponse.userCanceled; + const BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResultForConsume), + additionalStepBeforeReturn: (dynamic args) { + final String purchaseToken = args['purchaseToken'] as String; + consumeCompleter.complete(purchaseToken); + }); + + final Completer completer = Completer(); + PurchaseDetails purchaseDetails; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = purchaseStream.listen((_) { + purchaseDetails = _.first; + completer.complete(purchaseDetails); + subscription.cancel(); + }, onDone: () {}); + final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + applicationUserName: accountId); + await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); + + // Verify that the result has an error for the failed consumption + final GooglePlayPurchaseDetails result = + await completer.future as GooglePlayPurchaseDetails; + expect(result.status, PurchaseStatus.canceled); + }); + + test( + 'should get purchased purchase status when upgrading subscription by deferred proration mode', + () async { + const SkuDetailsWrapper skuDetails = dummySkuDetails; + const String accountId = 'hashedAccountId'; + const String debugMessage = 'dummy message'; + const BillingResponse sentCode = BillingResponse.ok; + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (dynamic _) { + // Mock java update purchase callback. + final MethodCall call = + MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': const BillingResponseConverter().toJson(sentCode), + 'purchasesList': const [] + }); + iapAndroidPlatform.billingClient.callHandler(call); + }); + + final Completer completer = Completer(); + PurchaseDetails purchaseDetails; + final Stream> purchaseStream = + iapAndroidPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = purchaseStream.listen((_) { + purchaseDetails = _.first; + completer.complete(purchaseDetails); + subscription.cancel(); + }, onDone: () {}); + final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + applicationUserName: accountId, + changeSubscriptionParam: ChangeSubscriptionParam( + oldPurchaseDetails: GooglePlayPurchaseDetails.fromPurchase( + dummyUnacknowledgedPurchase), + prorationMode: ProrationMode.deferred, + )); + await iapAndroidPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + final PurchaseDetails result = await completer.future; + expect(result.status, PurchaseStatus.purchased); + }); }); group('complete purchase', () { const String completeMethodName = 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; test('complete purchase success', () async { - final BillingResponse expectedCode = BillingResponse.ok; + const BillingResponse expectedCode = BillingResponse.ok; const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + const BillingResultWrapper expectedBillingResult = BillingResultWrapper( responseCode: expectedCode, debugMessage: debugMessage); stubPlatform.addResponse( name: completeMethodName, value: buildBillingResultMap(expectedBillingResult), ); - PurchaseDetails purchaseDetails = + final PurchaseDetails purchaseDetails = GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); - Completer completer = Completer(); + final Completer completer = + Completer(); purchaseDetails.status = PurchaseStatus.purchased; if (purchaseDetails.pendingCompletePurchase) { final BillingResultWrapper billingResultWrapper = diff --git a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart index 11a3426335d5..75972e644faa 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart @@ -5,11 +5,12 @@ import 'dart:async'; import 'package:flutter/services.dart'; -typedef void AdditionalSteps(dynamic args); +typedef AdditionalSteps = void Function(dynamic args); class StubInAppPurchasePlatform { - Map _expectedCalls = {}; - Map _additionalSteps = {}; + final Map _expectedCalls = {}; + final Map _additionalSteps = + {}; void addResponse( {required String name, dynamic value, @@ -18,7 +19,7 @@ class StubInAppPurchasePlatform { _expectedCalls[name] = value; } - List _previousCalls = []; + final List _previousCalls = []; List get previousCalls => _previousCalls; MethodCall previousCallMatching(String name) => _previousCalls.firstWhere((MethodCall call) => call.method == name); diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md deleted file mode 100644 index 04509e56ecde..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ /dev/null @@ -1,52 +0,0 @@ -## 0.1.3+4 - -* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. - -## 0.1.3+3 - -* Add `implements` to pubspec. - -# 0.1.3+2 - -* Removed dependency on the `test` package. - -# 0.1.3+1 - -- Updated installation instructions in README. - -## 0.1.3 - -* Add price symbol to platform interface object ProductDetail. - -## 0.1.2+2 - -* Fix crash when retrieveReceiptWithError gives an error. - -## 0.1.2+1 - -* Fix wrong data type when cancelling user credentials dialog. - -## 0.1.2 - -* Added countryCode to the SKPriceLocaleWrapper. - -## 0.1.1+1 - -* iOS: Fix treating missing App Store receipt as an exception. - -## 0.1.1 - -* Added support to register a `SKPaymentQueueDelegateWrapper` and handle changes to active subscriptions accordingly (see also Store Kit's [SKPaymentQueueDelegate](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc)). - -## 0.1.0+2 - -* Changed the iOS payment queue handler in such a way that it only adds a listener to the `SKPaymentQueue` when there - is a listener to the Dart `purchaseStream`. - -## 0.1.0+1 - -* Added a "Restore purchases" button to conform to Apple's StoreKit guidelines on [restoring products](https://developer.apple.com/documentation/storekit/in-app_purchase/restoring_purchased_products?language=objc); - -## 0.1.0 - -* Initial open-source release. diff --git a/packages/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md deleted file mode 100644 index 7ac21c495f7b..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# in\_app\_purchase\_ios - -The iOS implementation of [`in_app_purchase`][1]. - -## Usage - -This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` -as a dependency in your `pubspec.yaml`. This package will be automatically included in your app -when you do. - -If you wish to use the iOS package only, you can [add `in_app_purchase_ios` directly][3]. - -## Contributing - -This plugin uses -[json_serializable](https://pub.dev/packages/json_serializable) for the -many data structs passed between the underlying platform layers and Dart. After -editing any of the serialized data structs, rebuild the serializers by running -`flutter packages pub run build_runner build --delete-conflicting-outputs`. -`flutter packages pub run build_runner watch --delete-conflicting-outputs` will -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). - - -[1]: ../in_app_purchase -[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin -[3]: https://pub.dev/packages/in_app_purchase_ios/install diff --git a/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/in_app_purchase/in_app_purchase_ios/build.yaml b/packages/in_app_purchase/in_app_purchase_ios/build.yaml deleted file mode 100644 index e15cf14b85fd..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/build.yaml +++ /dev/null @@ -1,7 +0,0 @@ -targets: - $default: - builders: - json_serializable: - options: - any_map: true - create_to_json: true diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index a88050193053..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,676 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1630769A874F9381BC761FE1 /* libPods-Runner.a */; }; - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 688DE35021F2A5A100EA2684 /* TranslatorTests.m */; }; - 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */; }; - 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34B21EEB4B800D37AEF /* Stubs.m */; }; - 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18D02AB334F1C07BB9A4374A /* 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 */; }; - 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 */; }; - F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTests.m */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - A59001A921E69658004A3E5E /* 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 */ - 10B860DFD91A1DF639D7BE1D /* 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 = ""; }; - 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 = ""; }; - 1630769A874F9381BC761FE1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2550EB3A5A3E749A54ADCA2D /* 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 = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 688DE35021F2A5A100EA2684 /* TranslatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TranslatorTests.m; sourceTree = ""; }; - 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ProductRequestHandlerTests.m; sourceTree = ""; }; - 6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stubs.h; sourceTree = ""; }; - 6896B34B21EEB4B800D37AEF /* Stubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Stubs.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 = ""; }; - 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 = ""; }; - 9D681E092EB0D20D652F69FC /* 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 = ""; }; - A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - A59001A421E69658004A3E5E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppPurchasePluginTests.m; sourceTree = ""; }; - 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 = ""; }; - 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 */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, - 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A59001A121E69658004A3E5E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 0B4403AC68C3196AECF5EF89 /* Pods */ = { - isa = PBXGroup; - children = ( - E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */, - 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */, - 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */, - 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 334733E826680E5900DCC49E /* Temp */ = { - isa = PBXGroup; - children = ( - ); - path = Temp; - 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 = ( - 334733E826680E5900DCC49E /* Temp */, - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - A59001A521E69658004A3E5E /* RunnerTests */, - 97C146EF1CF9000F007C117D /* Products */, - E4DB99639FAD8ADED6B572FC /* Frameworks */, - 0B4403AC68C3196AECF5EF89 /* Pods */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - A59001A421E69658004A3E5E /* 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 */, - F6E5D5F926131C4800C68BED /* Configuration.storekit */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - A59001A521E69658004A3E5E /* RunnerTests */ = { - isa = PBXGroup; - children = ( - A59001A821E69658004A3E5E /* Info.plist */, - 6896B34A21EEB4B800D37AEF /* Stubs.h */, - 6896B34B21EEB4B800D37AEF /* Stubs.m */, - A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */, - 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */, - F78AF3132342BC89008449C7 /* PaymentQueueTests.m */, - 688DE35021F2A5A100EA2684 /* TranslatorTests.m */, - F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */, - ); - path = RunnerTests; - sourceTree = ""; - }; - E4DB99639FAD8ADED6B572FC /* Frameworks */ = { - isa = PBXGroup; - children = ( - A5279297219369C600FF69E6 /* StoreKit.framework */, - 1630769A874F9381BC761FE1 /* libPods-Runner.a */, - 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - EDD921296E29F853F7B69716 /* [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"; - }; - A59001A321E69658004A3E5E /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */, - A59001A021E69658004A3E5E /* Sources */, - A59001A121E69658004A3E5E /* Frameworks */, - A59001A221E69658004A3E5E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A59001AA21E69658004A3E5E /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = A59001A421E69658004A3E5E /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - SystemCapabilities = { - com.apple.InAppPurchase = { - enabled = 1; - }; - }; - }; - A59001A321E69658004A3E5E = { - CreatedOnToolsVersion = 10.0; - 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 */, - A59001A321E69658004A3E5E /* 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; - }; - A59001A221E69658004A3E5E /* 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"; - }; - 95C7A5986B77A8DF76F6DF3A /* [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"; - }; - EDD921296E29F853F7B69716 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A59001A021E69658004A3E5E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */, - F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */, - 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */, - 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */, - A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */, - 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - A59001AA21E69658004A3E5E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = A59001A921E69658004A3E5E /* 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; - CURRENT_PROJECT_VERSION = 1; - 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.inAppPurchaseExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; - 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.inAppPurchaseExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; - A59001AB21E69658004A3E5E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = RunnerTests/Info.plist; - 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.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - A59001AC21E69658004A3E5E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - 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; - }; -/* 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; - }; - A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A59001AB21E69658004A3E5E /* Debug */, - A59001AC21E69658004A3E5E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bd47ecb9ec0..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m deleted file mode 100644 index b51f622e939b..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ /dev/null @@ -1,441 +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 "FIAPaymentQueueHandler.h" -#import "Stubs.h" - -@import in_app_purchase_ios; - -@interface InAppPurchasePluginTest : XCTestCase - -@property(strong, nonatomic) FIAPReceiptManagerStub* receiptManagerStub; -@property(strong, nonatomic) InAppPurchasePlugin* plugin; - -@end - -@implementation InAppPurchasePluginTest - -- (void)setUp { - self.receiptManagerStub = [FIAPReceiptManagerStub new]; - self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; -} - -- (void)tearDown { -} - -- (void)testInvalidMethodCall { - XCTestExpectation* expectation = - [self expectationWithDescription:@"expect result to be not implemented"]; - FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; - __block id result; - [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(result, FlutterMethodNotImplemented); -} - -- (void)testCanMakePayments { - XCTestExpectation* expectation = [self expectationWithDescription:@"expect result to be YES"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" - arguments:NULL]; - __block id result; - [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(result, @YES); -} - -- (void)testGetProductResponse { - XCTestExpectation* expectation = - [self expectationWithDescription:@"expect response contains 1 item"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" - arguments:@[ @"123" ]]; - __block id result; - [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssert([result isKindOfClass:[NSDictionary class]]); - NSArray* resultArray = [result objectForKey:@"products"]; - XCTAssertEqual(resultArray.count, 1); - XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); -} - -- (void)testAddPaymentFailure { - XCTestExpectation* expectation = - [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 = 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]; - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); -} - -- (void)testAddPaymentSuccessWithMockQueue { - XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return success 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; - [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]; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); -} - -- (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:@{ - @"productIdentifier" : @"123", - @"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]; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); -} - -- (void)testRestoreTransactions { - XCTestExpectation* expectation = - [self expectationWithDescription:@"result successfully restore transactions"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" - arguments:nil]; - SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - __block BOOL callbackInvoked = NO; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:^() { - callbackInvoked = YES; - [expectation fulfill]; - } - shouldAddStorePayment:nil - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(callbackInvoked); -} - -- (void)testRetrieveReceiptDataSuccess { - XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; - __block NSDictionary* result; - [self.plugin handleMethodCall:call - result:^(id r) { - result = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(result); - XCTAssert([result isKindOfClass:[NSString class]]); -} - -- (void)testRetrieveReceiptDataError { - XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; - __block NSDictionary* result; - self.receiptManagerStub.returnError = YES; - [self.plugin handleMethodCall:call - result:^(id r) { - result = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(result); - XCTAssert([result isKindOfClass:[FlutterError class]]); - NSDictionary* details = ((FlutterError*)result).details; - XCTAssertNotNil(details[@"error"]); - NSNumber* errorCode = (NSNumber*)details[@"error"][@"code"]; - XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); -} - -- (void)testRefreshReceiptRequest { - XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" - arguments:nil]; - __block BOOL result = NO; - [self.plugin handleMethodCall:call - result:^(id r) { - result = YES; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(result); -} - -- (void)testPresentCodeRedemptionSheet { - XCTestExpectation* expectation = - [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - arguments:nil]; - __block BOOL callbackInvoked = NO; - [self.plugin handleMethodCall:call - result:^(id r) { - callbackInvoked = YES; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(callbackInvoked); -} - -- (void)testGetPendingTransactions { - XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; - SKPaymentQueue* mockQueue = OCMClassMock(SKPaymentQueue.class); - NSDictionary* transactionMap = @{ - @"transactionIdentifier" : [NSNull null], - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : [NSNull null], - }; - OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] - 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 handleMethodCall:call - result:^(id r) { - resultArray = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqualObjects(resultArray, @[ transactionMap ]); -} - -- (void)testStartAndStopObservingPaymentQueue { - 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 - [self.plugin handleMethodCall:startCall - result:^(id r){ - }]; - - // Observer should be set - XCTAssertNotNil(queue.observer); - - // Stop observing - [self.plugin handleMethodCall:stopCall - result:^(id r){ - }]; - - // No observer should be set - XCTAssertNil(queue.observer); -} - -- (void)testRegisterPaymentQueueDelegate { - if (@available(iOS 13, *)) { - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" - arguments:nil]; - - self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil]; - - // Verify the delegate is nil before we register one. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - - // Verify the delegate is not nil after we registered one. - XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - } -} - -- (void)testRemovePaymentQueueDelegate { - if (@available(iOS 13, *)) { - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" - arguments:nil]; - - self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil]; - self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); - - // Verify the delegate is not nil before removing it. - XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - - // Verify the delegate is nill after removing it. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - } -} - -- (void)testShowPriceConsentIfNeeded { - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" - arguments:nil]; - - FIAPaymentQueueHandler* mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); - self.plugin.paymentQueueHandler = mockQueueHandler; - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - if (@available(iOS 13.4, *)) { - OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); - } else { - OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); - } -#pragma clang diagnostic pop -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/PaymentQueueTests.m deleted file mode 100644 index 6cfbd278a429..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/PaymentQueueTests.m +++ /dev/null @@ -1,212 +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 "Stubs.h" - -@import in_app_purchase_ios; - -@interface PaymentQueueTest : XCTestCase - -@property(strong, nonatomic) NSDictionary *periodMap; -@property(strong, nonatomic) NSDictionary *discountMap; -@property(strong, nonatomic) NSDictionary *productMap; -@property(strong, nonatomic) NSDictionary *productResponseMap; - -@end - -@implementation PaymentQueueTest - -- (void)setUp { - self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; - self.discountMap = @{ - @"price" : @1.0, - @"currencyCode" : @"USD", - @"numberOfPeriods" : @1, - @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1 - }; - self.productMap = @{ - @"price" : @1.0, - @"currencyCode" : @"USD", - @"productIdentifier" : @"123", - @"localizedTitle" : @"title", - @"localizedDescription" : @"des", - @"subscriptionPeriod" : self.periodMap, - @"introductoryPrice" : self.discountMap, - @"subscriptionGroupIdentifier" : @"com.group" - }; - self.productResponseMap = - @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : [NSNull null]}; -} - -- (void)testTransactionPurchased { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get purchased transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchased; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased); - XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); -} - -- (void)testTransactionFailed { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get failed transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateFailed; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed); - XCTAssertEqual(tran.transactionIdentifier, nil); -} - -- (void)testTransactionRestored { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get restored transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateRestored; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored); - XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); -} - -- (void)testTransactionPurchasing { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get purchasing transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchasing; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing); - XCTAssertEqual(tran.transactionIdentifier, nil); -} - -- (void)testTransactionDeferred { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get deffered transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateDeferred; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred); - XCTAssertEqual(tran.transactionIdentifier, nil); -} - -- (void)testFinishTransaction { - XCTestExpectation *expectation = - [self expectationWithDescription:@"handler.transactions should be empty."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateDeferred; - __block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(transactions.count, 1); - SKPaymentTransaction *transaction = transactions[0]; - [handler finishTransaction:transaction]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(transactions.count, 1); - [expectation fulfill]; - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m deleted file mode 100644 index 89a7b2c84380..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m +++ /dev/null @@ -1,178 +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 "Stubs.h" - -@import in_app_purchase_ios; - -@interface TranslatorTest : XCTestCase - -@property(strong, nonatomic) NSDictionary *periodMap; -@property(strong, nonatomic) NSDictionary *discountMap; -@property(strong, nonatomic) NSMutableDictionary *productMap; -@property(strong, nonatomic) NSDictionary *productResponseMap; -@property(strong, nonatomic) NSDictionary *paymentMap; -@property(strong, nonatomic) NSDictionary *transactionMap; -@property(strong, nonatomic) NSDictionary *errorMap; -@property(strong, nonatomic) NSDictionary *localeMap; -@property(strong, nonatomic) NSDictionary *storefrontMap; -@property(strong, nonatomic) NSDictionary *storefrontAndPaymentTransactionMap; - -@end - -@implementation TranslatorTest - -- (void)setUp { - self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; - self.discountMap = @{ - @"price" : @"1", - @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], - @"numberOfPeriods" : @1, - @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1 - }; - - self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : @"1", - @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], - @"productIdentifier" : @"123", - @"localizedTitle" : @"title", - @"localizedDescription" : @"des", - }]; - if (@available(iOS 11.2, *)) { - self.productMap[@"subscriptionPeriod"] = self.periodMap; - self.productMap[@"introductoryPrice"] = self.discountMap; - } - - if (@available(iOS 12.0, *)) { - self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; - } - - self.productResponseMap = - @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; - self.paymentMap = @{ - @"productIdentifier" : @"123", - @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", - @"quantity" : @(2), - @"applicationUsername" : @"app user name", - @"simulatesAskToBuyInSandbox" : @(NO) - }; - NSDictionary *originalTransactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : [NSNull null], - }; - self.transactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : originalTransactionMap, - }; - self.errorMap = @{ - @"code" : @(123), - @"domain" : @"test_domain", - @"userInfo" : @{ - @"key" : @"value", - } - }; - self.storefrontMap = @{ - @"countryCode" : @"USA", - @"identifier" : @"unique_identifier", - }; - - self.storefrontAndPaymentTransactionMap = @{ - @"storefront" : self.storefrontMap, - @"transaction" : self.transactionMap, - }; -} - -- (void)testSKProductSubscriptionPeriodStubToMap { - if (@available(iOS 11.2, *)) { - SKProductSubscriptionPeriodStub *period = - [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; - XCTAssertEqualObjects(map, self.periodMap); - } -} - -- (void)testSKProductDiscountStubToMap { - if (@available(iOS 11.2, *)) { - SKProductDiscountStub *discount = [[SKProductDiscountStub alloc] initWithMap:self.discountMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; - XCTAssertEqualObjects(map, self.discountMap); - } -} - -- (void)testProductToMap { - SKProductStub *product = [[SKProductStub alloc] initWithMap:self.productMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProduct:product]; - XCTAssertEqualObjects(map, self.productMap); -} - -- (void)testProductResponseToMap { - SKProductsResponseStub *response = - [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; - XCTAssertEqualObjects(map, self.productResponseMap); -} - -- (void)testPaymentToMap { - SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:self.paymentMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKPayment:payment]; - XCTAssertEqualObjects(map, self.paymentMap); -} - -- (void)testPaymentTransactionToMap { - // payment is not KVC, cannot test payment field. - SKPaymentTransactionStub *paymentTransaction = - [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; - XCTAssertEqualObjects(map, self.transactionMap); -} - -- (void)testError { - NSErrorStub *error = [[NSErrorStub alloc] initWithMap:self.errorMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; - XCTAssertEqualObjects(map, self.errorMap); -} - -- (void)testLocaleToMap { - if (@available(iOS 10.0, *)) { - NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; - NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; - XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); - XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); - } -} - -- (void)testSKStorefrontToMap { - if (@available(iOS 13.0, *)) { - SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront]; - XCTAssertEqualObjects(map, self.storefrontMap); - } -} - -- (void)testSKStorefrontAndSKPaymentTransactionToMap { - if (@available(iOS 13.0, *)) { - SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; - SKPaymentTransaction *transaction = - [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront - andSKPaymentTransaction:transaction]; - XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); - } -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart deleted file mode 100644 index 19884745bce8..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_ios_example/example_payment_queue_delegate.dart'; -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'consumable_store.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - - // When using the Android plugin directly it is mandatory to register - // the plugin as default instance as part of initializing the app. - InAppPurchaseIosPlatform.registerPlatform(); - - runApp(_MyApp()); -} - -const bool _kAutoConsume = true; - -const String _kConsumableId = 'consumable'; -const String _kUpgradeId = 'upgrade'; -const String _kSilverSubscriptionId = 'subscription_silver'; -const String _kGoldSubscriptionId = 'subscription_gold'; -const List _kProductIds = [ - _kConsumableId, - _kUpgradeId, - _kSilverSubscriptionId, - _kGoldSubscriptionId, -]; - -class _MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State<_MyApp> { - final InAppPurchaseIosPlatform _iapIosPlatform = - InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; - final InAppPurchaseIosPlatformAddition _iapIosPlatformAddition = - InAppPurchasePlatformAddition.instance - as InAppPurchaseIosPlatformAddition; - late StreamSubscription> _subscription; - List _notFoundIds = []; - List _products = []; - List _purchases = []; - List _consumables = []; - bool _isAvailable = false; - bool _purchasePending = false; - bool _loading = true; - String? _queryProductError; - - @override - void initState() { - final Stream> purchaseUpdated = - _iapIosPlatform.purchaseStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { - _listenToPurchaseUpdated(purchaseDetailsList); - }, onDone: () { - _subscription.cancel(); - }, onError: (error) { - // handle error here. - }); - - // Register the example payment queue delegate - _iapIosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); - - initStoreInfo(); - super.initState(); - } - - Future initStoreInfo() async { - final bool isAvailable = await _iapIosPlatform.isAvailable(); - if (!isAvailable) { - setState(() { - _isAvailable = isAvailable; - _products = []; - _purchases = []; - _notFoundIds = []; - _consumables = []; - _purchasePending = false; - _loading = false; - }); - return; - } - - ProductDetailsResponse productDetailResponse = - await _iapIosPlatform.queryProductDetails(_kProductIds.toSet()); - if (productDetailResponse.error != null) { - setState(() { - _queryProductError = productDetailResponse.error!.message; - _isAvailable = isAvailable; - _products = productDetailResponse.productDetails; - _purchases = []; - _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = []; - _purchasePending = false; - _loading = false; - }); - return; - } - - if (productDetailResponse.productDetails.isEmpty) { - setState(() { - _queryProductError = null; - _isAvailable = isAvailable; - _products = productDetailResponse.productDetails; - _purchases = []; - _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = []; - _purchasePending = false; - _loading = false; - }); - return; - } - - List consumables = await ConsumableStore.load(); - setState(() { - _isAvailable = isAvailable; - _products = productDetailResponse.productDetails; - _notFoundIds = productDetailResponse.notFoundIDs; - _consumables = consumables; - _purchasePending = false; - _loading = false; - }); - } - - @override - void dispose() { - _subscription.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - List stack = []; - if (_queryProductError == null) { - stack.add( - ListView( - children: [ - _buildConnectionCheckTile(), - _buildProductList(), - _buildConsumableBox(), - _buildRestoreButton(), - ], - ), - ); - } else { - stack.add(Center( - child: Text(_queryProductError!), - )); - } - if (_purchasePending) { - stack.add( - Stack( - children: [ - Opacity( - opacity: 0.3, - child: const ModalBarrier(dismissible: false, color: Colors.grey), - ), - Center( - child: CircularProgressIndicator(), - ), - ], - ), - ); - } - - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('IAP Example'), - ), - body: Stack( - children: stack, - ), - ), - ); - } - - Card _buildConnectionCheckTile() { - if (_loading) { - return Card(child: ListTile(title: const Text('Trying to connect...'))); - } - 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') + '.'), - ); - final List children = [storeHeader]; - - if (!_isAvailable) { - children.addAll([ - Divider(), - ListTile( - title: Text('Not connected', - style: TextStyle(color: ThemeData.light().errorColor)), - subtitle: const Text( - 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), - ), - ]); - } - return Card(child: Column(children: children)); - } - - Card _buildProductList() { - if (_loading) { - return Card( - child: (ListTile( - leading: CircularProgressIndicator(), - title: Text('Fetching products...')))); - } - if (!_isAvailable) { - return Card(); - } - final ListTile productHeader = ListTile(title: Text('Products for Sale')); - List productList = []; - if (_notFoundIds.isNotEmpty) { - productList.add(ListTile( - title: Text('[${_notFoundIds.join(", ")}] not found', - style: TextStyle(color: ThemeData.light().errorColor)), - subtitle: Text( - 'This app needs special configuration to run. Please see example/README.md for instructions.'))); - } - - // This loading previous purchases code is just a demo. Please do not use this as it is. - // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it. - // We recommend that you use your own server to verify the purchase data. - Map purchases = - Map.fromEntries(_purchases.map((PurchaseDetails purchase) { - if (purchase.pendingCompletePurchase) { - _iapIosPlatform.completePurchase(purchase); - } - return MapEntry(purchase.productID, purchase); - })); - productList.addAll(_products.map( - (ProductDetails productDetails) { - PurchaseDetails? previousPurchase = purchases[productDetails.id]; - return ListTile( - title: Text( - productDetails.title, - ), - subtitle: Text( - productDetails.description, - ), - trailing: previousPurchase != null - ? IconButton( - onPressed: () { - _iapIosPlatformAddition.showPriceConsentIfNeeded(); - }, - icon: Icon(Icons.upgrade)) - : TextButton( - child: Text(productDetails.price), - style: TextButton.styleFrom( - backgroundColor: Colors.green[800], - primary: Colors.white, - ), - onPressed: () { - PurchaseParam purchaseParam = PurchaseParam( - productDetails: productDetails, - applicationUserName: null, - ); - if (productDetails.id == _kConsumableId) { - _iapIosPlatform.buyConsumable( - purchaseParam: purchaseParam, - autoConsume: _kAutoConsume || Platform.isIOS); - } else { - _iapIosPlatform.buyNonConsumable( - purchaseParam: purchaseParam); - } - }, - )); - }, - )); - - return Card( - child: - Column(children: [productHeader, Divider()] + productList)); - } - - Card _buildConsumableBox() { - if (_loading) { - return Card( - child: (ListTile( - leading: CircularProgressIndicator(), - title: Text('Fetching consumables...')))); - } - if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { - return Card(); - } - final ListTile consumableHeader = - ListTile(title: Text('Purchased consumables')); - final List tokens = _consumables.map((String id) { - return GridTile( - child: IconButton( - icon: Icon( - Icons.stars, - size: 42.0, - color: Colors.orange, - ), - splashColor: Colors.yellowAccent, - onPressed: () => consume(id), - ), - ); - }).toList(); - return Card( - child: Column(children: [ - consumableHeader, - Divider(), - GridView.count( - crossAxisCount: 5, - children: tokens, - shrinkWrap: true, - padding: EdgeInsets.all(16.0), - ) - ])); - } - - Widget _buildRestoreButton() { - if (_loading) { - return Container(); - } - - return Padding( - padding: const EdgeInsets.all(4.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - child: Text('Restore purchases'), - style: TextButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - primary: Colors.white, - ), - onPressed: () => _iapIosPlatform.restorePurchases(), - ), - ], - ), - ); - } - - Future consume(String id) async { - await ConsumableStore.consume(id); - final List consumables = await ConsumableStore.load(); - setState(() { - _consumables = consumables; - }); - } - - void showPendingUI() { - setState(() { - _purchasePending = true; - }); - } - - void deliverProduct(PurchaseDetails purchaseDetails) async { - // IMPORTANT!! Always verify purchase details before delivering the product. - if (purchaseDetails.productID == _kConsumableId) { - await ConsumableStore.save(purchaseDetails.purchaseID!); - List consumables = await ConsumableStore.load(); - setState(() { - _purchasePending = false; - _consumables = consumables; - }); - } else { - setState(() { - _purchases.add(purchaseDetails); - _purchasePending = false; - }); - } - } - - void handleError(IAPError error) { - setState(() { - _purchasePending = false; - }); - } - - Future _verifyPurchase(PurchaseDetails purchaseDetails) { - // IMPORTANT!! Always verify a purchase before delivering the product. - // For the purpose of an example, we directly return true. - return Future.value(true); - } - - void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { - // handle invalid purchase here if _verifyPurchase` failed. - } - - void _listenToPurchaseUpdated(List purchaseDetailsList) { - purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { - if (purchaseDetails.status == PurchaseStatus.pending) { - showPendingUI(); - } else { - if (purchaseDetails.status == PurchaseStatus.error) { - handleError(purchaseDetails.error!); - } else if (purchaseDetails.status == PurchaseStatus.purchased) { - bool valid = await _verifyPurchase(purchaseDetails); - if (valid) { - deliverProduct(purchaseDetails); - } else { - _handleInvalidPurchase(purchaseDetails); - return; - } - } - - if (purchaseDetails.pendingCompletePurchase) { - await _iapIosPlatform.completePurchase(purchaseDetails); - } - } - }); - } -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml deleted file mode 100644 index 0474d70e8b71..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: in_app_purchase_ios_example -description: Demonstrates how to use the in_app_purchase_ios plugin. -publish_to: none - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - shared_preferences: ^2.0.0 - in_app_purchase_ios: - # When depending on this package from a real application you should use: - # in_app_purchase: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - - in_app_purchase_platform_interface: ^1.0.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m deleted file mode 100644 index 0125604b3b3c..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m +++ /dev/null @@ -1,196 +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 "FIAObjectTranslator.h" - -#pragma mark - SKProduct Coders - -@implementation FIAObjectTranslator - -+ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { - if (!product) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"localizedDescription" : product.localizedDescription ?: [NSNull null], - @"localizedTitle" : product.localizedTitle ?: [NSNull null], - @"productIdentifier" : product.productIdentifier ?: [NSNull null], - @"price" : product.price.description ?: [NSNull null] - - }]; - // 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. - // https://github.com/flutter/flutter/issues/26610 - [map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null] - forKey:@"priceLocale"]; - if (@available(iOS 11.2, *)) { - [map setObject:[FIAObjectTranslator - getMapFromSKProductSubscriptionPeriod:product.subscriptionPeriod] - ?: [NSNull null] - forKey:@"subscriptionPeriod"]; - } - if (@available(iOS 11.2, *)) { - [map setObject:[FIAObjectTranslator getMapFromSKProductDiscount:product.introductoryPrice] - ?: [NSNull null] - forKey:@"introductoryPrice"]; - } - if (@available(iOS 12.0, *)) { - [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null] - forKey:@"subscriptionGroupIdentifier"]; - } - return map; -} - -+ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period { - if (!period) { - return nil; - } - return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; -} - -+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { - if (!discount) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : discount.price.description ?: [NSNull null], - @"numberOfPeriods" : @(discount.numberOfPeriods), - @"subscriptionPeriod" : - [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] - ?: [NSNull null], - @"paymentMode" : @(discount.paymentMode) - }]; - - // 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. - // https://github.com/flutter/flutter/issues/26610 - [map setObject:[FIAObjectTranslator getMapFromNSLocale:discount.priceLocale] ?: [NSNull null] - forKey:@"priceLocale"]; - return map; -} - -+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse { - if (!productResponse) { - return nil; - } - NSMutableArray *productsMapArray = [NSMutableArray new]; - for (SKProduct *product in productResponse.products) { - [productsMapArray addObject:[FIAObjectTranslator getMapFromSKProduct:product]]; - } - return @{ - @"products" : productsMapArray, - @"invalidProductIdentifiers" : productResponse.invalidProductIdentifiers ?: @[] - }; -} - -+ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { - if (!payment) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"productIdentifier" : payment.productIdentifier ?: [NSNull null], - @"requestData" : payment.requestData ? [[NSString alloc] initWithData:payment.requestData - encoding:NSUTF8StringEncoding] - : [NSNull null], - @"quantity" : @(payment.quantity), - @"applicationUsername" : payment.applicationUsername ?: [NSNull null] - }]; - [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; - return map; -} - -+ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale { - if (!locale) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; - [map setObject:[locale objectForKey:NSLocaleCurrencySymbol] ?: [NSNull null] - forKey:@"currencySymbol"]; - [map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null] - forKey:@"currencyCode"]; - [map setObject:[locale objectForKey:NSLocaleCountryCode] ?: [NSNull null] forKey:@"countryCode"]; - return map; -} - -+ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { - if (!map) { - return nil; - } - SKMutablePayment *payment = [[SKMutablePayment alloc] init]; - payment.productIdentifier = map[@"productIdentifier"]; - NSString *utf8String = map[@"requestData"]; - payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; - payment.quantity = [map[@"quantity"] integerValue]; - payment.applicationUsername = map[@"applicationUsername"]; - payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; - return payment; -} - -+ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction { - if (!transaction) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"error" : [FIAObjectTranslator getMapFromNSError:transaction.error] ?: [NSNull null], - @"payment" : transaction.payment ? [FIAObjectTranslator getMapFromSKPayment:transaction.payment] - : [NSNull null], - @"originalTransaction" : transaction.originalTransaction - ? [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction.originalTransaction] - : [NSNull null], - @"transactionTimeStamp" : transaction.transactionDate - ? @(transaction.transactionDate.timeIntervalSince1970) - : [NSNull null], - @"transactionIdentifier" : transaction.transactionIdentifier ?: [NSNull null], - @"transactionState" : @(transaction.transactionState) - }]; - - return map; -} - -+ (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; - } - } - return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; -} - -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { - if (!storefront) { - return nil; - } - - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"countryCode" : storefront.countryCode, - @"identifier" : storefront.identifier - }]; - - return map; -} - -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront - andSKPaymentTransaction:(SKPaymentTransaction *)transaction { - if (!storefront || !transaction) { - return nil; - } - - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], - @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] - }]; - - return map; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h deleted file mode 100644 index 8019831d6355..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h +++ /dev/null @@ -1,60 +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 - -@class SKPaymentTransaction; - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^TransactionsUpdated)(NSArray *transactions); -typedef void (^TransactionsRemoved)(NSArray *transactions); -typedef void (^RestoreTransactionFailed)(NSError *error); -typedef void (^RestoreCompletedTransactionsFinished)(void); -typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); -typedef void (^UpdatedDownloads)(NSArray *downloads); - -@interface FIAPaymentQueueHandler : NSObject - -@property(NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE( - ios(13.0), macos(10.15), watchos(6.2)); - -- (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; -// 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; -- (void)presentCodeRedemptionSheet; -- (NSArray *)getUnfinishedTransactions; - -// This method needs to be called before any other methods. -- (void)startObservingPaymentQueue; -// Call this method when the Flutter app is no longer listening -- (void)stopObservingPaymentQueue; - -// Appends a payment to the SKPaymentQueue. -// -// @param payment Payment object to be added to the payment queue. -// @return whether "addPayment" was successful. -- (BOOL)addPayment:(SKPayment *)payment; - -// Displays the price consent sheet. -// -// The price consent sheet is only displayed when the following -// it true: -// - You have increased the price of the subscription in App Store Connect. -// - The subscriber has not yet responded to a price consent query. -// Otherwise the method has no effect. -- (void)showPriceConsentIfNeeded API_AVAILABLE(ios(13.4)); - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m deleted file mode 100644 index 21667954cf8d..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m +++ /dev/null @@ -1,135 +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 "FIAPaymentQueueHandler.h" -#import "FIAPPaymentQueueDelegate.h" - -@interface FIAPaymentQueueHandler () - -@property(strong, nonatomic) SKPaymentQueue *queue; -@property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated; -@property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved; -@property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed; -@property(nullable, copy, nonatomic) - RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished; -@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; -@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; - -@end - -@implementation FIAPaymentQueueHandler - -- (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 { - self = [super init]; - if (self) { - _queue = queue; - _transactionsUpdated = transactionsUpdated; - _transactionsRemoved = transactionsRemoved; - _restoreTransactionFailed = restoreTransactionFailed; - _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; - _shouldAddStorePayment = shouldAddStorePayment; - _updatedDownloads = updatedDownloads; - - if (@available(iOS 13.0, macOS 10.15, *)) { - queue.delegate = self.delegate; - } - } - return self; -} - -- (void)startObservingPaymentQueue { - [_queue addTransactionObserver:self]; -} - -- (void)stopObservingPaymentQueue { - [_queue removeTransactionObserver:self]; -} - -- (BOOL)addPayment:(SKPayment *)payment { - for (SKPaymentTransaction *transaction in self.queue.transactions) { - if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { - return NO; - } - } - [self.queue addPayment:payment]; - return YES; -} - -- (void)finishTransaction:(SKPaymentTransaction *)transaction { - [self.queue finishTransaction:transaction]; -} - -- (void)restoreTransactions:(nullable NSString *)applicationName { - if (applicationName) { - [self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName]; - } else { - [self.queue restoreCompletedTransactions]; - } -} - -- (void)presentCodeRedemptionSheet { - if (@available(iOS 14, *)) { - [self.queue presentCodeRedemptionSheet]; - } else { - NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); - } -} - -- (void)showPriceConsentIfNeeded { - [self.queue showPriceConsentIfNeeded]; -} - -#pragma mark - observing - -// Sent when the transaction array has changed (additions or state changes). Client should check -// state of transactions and finish as appropriate. -- (void)paymentQueue:(SKPaymentQueue *)queue - updatedTransactions:(NSArray *)transactions { - // notify dart through callbacks. - self.transactionsUpdated(transactions); -} - -// Sent when transactions are removed from the queue (via finishTransaction:). -- (void)paymentQueue:(SKPaymentQueue *)queue - removedTransactions:(NSArray *)transactions { - self.transactionsRemoved(transactions); -} - -// Sent when an error is encountered while adding transactions from the user's purchase history back -// to the queue. -- (void)paymentQueue:(SKPaymentQueue *)queue - restoreCompletedTransactionsFailedWithError:(NSError *)error { - self.restoreTransactionFailed(error); -} - -// Sent when all transactions from the user's purchase history have successfully been added back to -// the queue. -- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { - self.paymentQueueRestoreCompletedTransactionsFinished(); -} - -// Sent when the download state has changed. -- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { - self.updatedDownloads(downloads); -} - -// Sent when a user initiates an IAP buy from the App Store -- (BOOL)paymentQueue:(SKPaymentQueue *)queue - shouldAddStorePayment:(SKPayment *)payment - forProduct:(SKProduct *)product { - return (self.shouldAddStorePayment(payment, product)); -} - -- (NSArray *)getUnfinishedTransactions { - return self.queue.transactions; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec deleted file mode 100644 index 3d15b5c0d02c..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'in_app_purchase_ios' - s.version = '0.0.1' - s.summary = 'Flutter In App Purchase iOS' - s.description = <<-DESC -A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios' } - # TODO(mvanbeusekom): update URL when in_app_purchase_ios package is published. - # Updating it before the package is published will cause a lint error and block the tree. - s.documentation_url = 'https://pub.dev/packages/in_app_purchase' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart deleted file mode 100644 index 21e76815e6ac..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart +++ /dev/null @@ -1,7 +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. - -export 'src/in_app_purchase_ios_platform.dart'; -export 'src/in_app_purchase_ios_platform_addition.dart'; -export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart deleted file mode 100644 index 74bb898a3382..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:in_app_purchase_ios/src/in_app_purchase_ios_platform_addition.dart'; -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; - -import '../in_app_purchase_ios.dart'; -import '../store_kit_wrappers.dart'; - -/// [IAPError.code] code for failed purchases. -const String kPurchaseErrorCode = 'purchase_error'; - -/// Indicates store front is Apple AppStore. -const String kIAPSource = 'app_store'; - -/// An [InAppPurchasePlatform] that wraps StoreKit. -/// -/// This translates various `StoreKit` calls and responses into the -/// generic plugin API. -class InAppPurchaseIosPlatform extends InAppPurchasePlatform { - static late SKPaymentQueueWrapper _skPaymentQueueWrapper; - static late _TransactionObserver _observer; - - /// Creates an [InAppPurchaseIosPlatform] object. - /// - /// This constructor should only be used for testing, for any other purpose - /// get the connection from the [instance] getter. - @visibleForTesting - InAppPurchaseIosPlatform(); - - Stream> get purchaseStream => - _observer.purchaseUpdatedController.stream; - - /// Callback handler for transaction status changes. - @visibleForTesting - static SKTransactionObserverWrapper get observer => _observer; - - /// Registers this class as the default instance of [InAppPurchasePlatform]. - static void registerPlatform() { - // Register the [InAppPurchaseIosPlatformAddition] containing iOS - // platform-specific functionality. - InAppPurchasePlatformAddition.instance = InAppPurchaseIosPlatformAddition(); - - // Register the platform-specific implementation of the idiomatic - // InAppPurchase API. - InAppPurchasePlatform.instance = InAppPurchaseIosPlatform(); - - _skPaymentQueueWrapper = SKPaymentQueueWrapper(); - - // Create a purchaseUpdatedController and notify the native side when to - // start of stop sending updates. - StreamController> updateController = - StreamController.broadcast( - onListen: () => _skPaymentQueueWrapper.startObservingTransactionQueue(), - onCancel: () => _skPaymentQueueWrapper.stopObservingTransactionQueue(), - ); - _observer = _TransactionObserver(updateController); - _skPaymentQueueWrapper.setTransactionObserver(observer); - } - - @override - Future isAvailable() => SKPaymentQueueWrapper.canMakePayments(); - - @override - Future buyNonConsumable({required PurchaseParam purchaseParam}) async { - await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( - productIdentifier: purchaseParam.productDetails.id, - quantity: 1, - applicationUsername: purchaseParam.applicationUserName, - simulatesAskToBuyInSandbox: (purchaseParam is AppStorePurchaseParam) - ? purchaseParam.simulatesAskToBuyInSandbox - : false, - requestData: null)); - - return true; // There's no error feedback from iOS here to return. - } - - @override - Future buyConsumable( - {required PurchaseParam purchaseParam, bool autoConsume = true}) { - assert(autoConsume == true, 'On iOS, we should always auto consume'); - return buyNonConsumable(purchaseParam: purchaseParam); - } - - @override - Future completePurchase(PurchaseDetails purchase) { - assert( - purchase is AppStorePurchaseDetails, - 'On iOS, the `purchase` should always be of type `AppStorePurchaseDetails`.', - ); - - return _skPaymentQueueWrapper.finishTransaction( - (purchase as AppStorePurchaseDetails).skPaymentTransaction, - ); - } - - @override - Future restorePurchases({String? applicationUserName}) async { - return _observer - .restoreTransactions( - queue: _skPaymentQueueWrapper, - applicationUserName: applicationUserName) - .whenComplete(() => _observer.cleanUpRestoredTransactions()); - } - - /// Query the product detail list. - /// - /// This method only returns [ProductDetailsResponse]. - /// To get detailed Store Kit product list, use [SkProductResponseWrapper.startProductRequest] - /// to get the [SKProductResponseWrapper]. - @override - Future queryProductDetails( - Set identifiers) async { - final SKRequestMaker requestMaker = SKRequestMaker(); - SkProductResponseWrapper response; - PlatformException? exception; - try { - response = await requestMaker.startProductRequest(identifiers.toList()); - } on PlatformException catch (e) { - exception = e; - response = SkProductResponseWrapper( - products: [], invalidProductIdentifiers: identifiers.toList()); - } - List productDetails = []; - if (response.products != null) { - productDetails = response.products - .map((SKProductWrapper productWrapper) => - AppStoreProductDetails.fromSKProduct(productWrapper)) - .toList(); - } - List invalidIdentifiers = response.invalidProductIdentifiers; - if (productDetails.isEmpty) { - invalidIdentifiers = identifiers.toList(); - } - ProductDetailsResponse productDetailsResponse = ProductDetailsResponse( - productDetails: productDetails, - notFoundIDs: invalidIdentifiers, - error: exception == null - ? null - : IAPError( - source: kIAPSource, - code: exception.code, - message: exception.message ?? '', - details: exception.details), - ); - return productDetailsResponse; - } -} - -class _TransactionObserver implements SKTransactionObserverWrapper { - final StreamController> purchaseUpdatedController; - - Completer? _restoreCompleter; - late String _receiptData; - - _TransactionObserver(this.purchaseUpdatedController); - - Future restoreTransactions({ - required SKPaymentQueueWrapper queue, - String? applicationUserName, - }) { - _restoreCompleter = Completer(); - queue.restoreTransactions(applicationUserName: applicationUserName); - return _restoreCompleter!.future; - } - - void cleanUpRestoredTransactions() { - _restoreCompleter = null; - } - - void updatedTransactions( - {required List transactions}) async { - String receiptData = await getReceiptData(); - List purchases = transactions - .map((SKPaymentTransactionWrapper transaction) => - AppStorePurchaseDetails.fromSKTransaction(transaction, receiptData)) - .toList(); - - purchaseUpdatedController.add(purchases); - } - - void removedTransactions( - {required List transactions}) {} - - /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({required SKError error}) { - _restoreCompleter!.completeError(error); - } - - void paymentQueueRestoreCompletedTransactionsFinished() { - _restoreCompleter!.complete(); - } - - bool shouldAddStorePayment( - {required SKPaymentWrapper payment, required SKProductWrapper product}) { - // In this unified API, we always return true to keep it consistent with the behavior on Google Play. - return true; - } - - Future getReceiptData() async { - try { - _receiptData = await SKReceiptManager.retrieveReceiptData(); - } catch (e) { - _receiptData = ''; - } - return _receiptData; - } -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart deleted file mode 100644 index 359e51713521..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart +++ /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. - -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; - -import '../store_kit_wrappers.dart'; - -/// Contains InApp Purchase features that are only available on iOS. -class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition { - /// Present Code Redemption Sheet. - /// - /// Available on devices running iOS 14 and iPadOS 14 and later. - Future presentCodeRedemptionSheet() { - return SKPaymentQueueWrapper().presentCodeRedemptionSheet(); - } - - /// Retry loading purchase data after an initial failure. - /// - /// If no results, a `null` value is returned. - Future refreshPurchaseVerificationData() async { - await SKRequestMaker().startRefreshReceiptRequest(); - try { - String receipt = await SKReceiptManager.retrieveReceiptData(); - return PurchaseVerificationData( - localVerificationData: receipt, - serverVerificationData: receipt, - source: kIAPSource); - } catch (e) { - print( - 'Something is wrong while fetching the receipt, this normally happens when the app is ' - 'running on a simulator: $e'); - return null; - } - } - - /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. - /// - /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to - /// finish transactions when the storefront changes or if the price consent - /// sheet should be displayed when the price of a subscription has changed. If - /// no delegate is registered iOS will fallback to it's default configuration. - /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). - /// - /// When set to `null` the payment queue delegate will be removed and the - /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). - Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) => - SKPaymentQueueWrapper().setDelegate(delegate); - - /// Shows the price consent sheet if the user has not yet responded to a - /// subscription price change. - /// - /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] - /// (using the [setDelegate] method) and returned `false` when the - /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. - /// - /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). - Future showPriceConsentIfNeeded() => - SKPaymentQueueWrapper().showPriceConsentIfNeeded(); -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart deleted file mode 100644 index 08af2c6058c4..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import '../../store_kit_wrappers.dart'; - -part 'enum_converters.g.dart'; - -/// Serializer for [SKPaymentTransactionStateWrapper]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SKTransactionStatusConverter()`. -class SKTransactionStatusConverter - implements JsonConverter { - /// Default const constructor. - const SKTransactionStatusConverter(); - - @override - SKPaymentTransactionStateWrapper fromJson(int? json) { - if (json == null) { - return SKPaymentTransactionStateWrapper.unspecified; - } - return _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap - .cast(), - json); - } - - /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus]. - PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) { - switch (object) { - case SKPaymentTransactionStateWrapper.purchasing: - case SKPaymentTransactionStateWrapper.deferred: - return PurchaseStatus.pending; - case SKPaymentTransactionStateWrapper.purchased: - return PurchaseStatus.purchased; - case SKPaymentTransactionStateWrapper.restored: - return PurchaseStatus.restored; - case SKPaymentTransactionStateWrapper.failed: - case SKPaymentTransactionStateWrapper.unspecified: - return PurchaseStatus.error; - } - } - - @override - int toJson(SKPaymentTransactionStateWrapper object) => - _$SKPaymentTransactionStateWrapperEnumMap[object]!; -} - -/// Serializer for [SKSubscriptionPeriodUnit]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SKSubscriptionPeriodUnitConverter()`. -class SKSubscriptionPeriodUnitConverter - implements JsonConverter { - /// Default const constructor. - const SKSubscriptionPeriodUnitConverter(); - - @override - SKSubscriptionPeriodUnit fromJson(int? json) { - if (json == null) { - return SKSubscriptionPeriodUnit.day; - } - return _$enumDecode( - _$SKSubscriptionPeriodUnitEnumMap - .cast(), - json); - } - - @override - int toJson(SKSubscriptionPeriodUnit object) => - _$SKSubscriptionPeriodUnitEnumMap[object]!; -} - -/// Serializer for [SKProductDiscountPaymentMode]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SKProductDiscountPaymentModeConverter()`. -class SKProductDiscountPaymentModeConverter - implements JsonConverter { - /// Default const constructor. - const SKProductDiscountPaymentModeConverter(); - - @override - SKProductDiscountPaymentMode fromJson(int? json) { - if (json == null) { - return SKProductDiscountPaymentMode.payAsYouGo; - } - return _$enumDecode( - _$SKProductDiscountPaymentModeEnumMap - .cast(), - json); - } - - @override - int toJson(SKProductDiscountPaymentMode object) => - _$SKProductDiscountPaymentModeEnumMap[object]!; -} - -// Define a class so we generate serializer helper methods for the enums -@JsonSerializable() -class _SerializedEnums { - late SKPaymentTransactionStateWrapper response; - late SKSubscriptionPeriodUnit unit; - late SKProductDiscountPaymentMode discountPaymentMode; -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart deleted file mode 100644 index b003f435a800..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart +++ /dev/null @@ -1,73 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'enum_converters.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_SerializedEnums _$_SerializedEnumsFromJson(Map json) { - return _SerializedEnums() - ..response = _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap, json['response']) - ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) - ..discountPaymentMode = _$enumDecode( - _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); -} - -Map _$_SerializedEnumsToJson(_SerializedEnums instance) => - { - 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], - 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], - 'discountPaymentMode': - _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode], - }; - -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - -const _$SKPaymentTransactionStateWrapperEnumMap = { - SKPaymentTransactionStateWrapper.purchasing: 0, - SKPaymentTransactionStateWrapper.purchased: 1, - SKPaymentTransactionStateWrapper.failed: 2, - SKPaymentTransactionStateWrapper.restored: 3, - SKPaymentTransactionStateWrapper.deferred: 4, - SKPaymentTransactionStateWrapper.unspecified: -1, -}; - -const _$SKSubscriptionPeriodUnitEnumMap = { - SKSubscriptionPeriodUnit.day: 0, - SKSubscriptionPeriodUnit.week: 1, - SKSubscriptionPeriodUnit.month: 2, - SKSubscriptionPeriodUnit.year: 3, -}; - -const _$SKProductDiscountPaymentModeEnumMap = { - SKProductDiscountPaymentMode.payAsYouGo: 0, - SKProductDiscountPaymentMode.payUpFront: 1, - SKProductDiscountPaymentMode.freeTrail: 2, - SKProductDiscountPaymentMode.unspecified: -1, -}; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart deleted file mode 100644 index 079e75078037..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ /dev/null @@ -1,476 +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' show hashValues; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:meta/meta.dart'; - -import '../channel.dart'; -import '../in_app_purchase_ios_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'; - -/// A wrapper around -/// [`SKPaymentQueue`](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc). -/// -/// The payment queue contains payment related operations. It communicates with -/// the App Store and presents a user interface for the user to process and -/// authorize payments. -/// -/// Full information on using `SKPaymentQueue` and processing purchases is -/// available at the [In-App Purchase Programming -/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). -class SKPaymentQueueWrapper { - /// Returns the default payment queue. - /// - /// We do not support instantiating a custom payment queue, hence the - /// singleton. However, you can override the observer. - factory SKPaymentQueueWrapper() { - return _singleton; - } - - SKPaymentQueueWrapper._(); - - static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); - - SKPaymentQueueDelegateWrapper? _paymentQueueDelegate; - SKTransactionObserverWrapper? _observer; - - /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) - Future> transactions() async { - return _getTransactionList((await channel - .invokeListMethod('-[SKPaymentQueue transactions]'))!); - } - - /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). - static Future canMakePayments() async => - (await channel - .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? - false; - - /// Sets an observer to listen to all incoming transaction events. - /// - /// This should be called and set as soon as the app launches in order to - /// avoid missing any purchase updates from the App Store. See the - /// documentation on StoreKit's [`-[SKPaymentQueue - /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). - void setTransactionObserver(SKTransactionObserverWrapper observer) { - _observer = observer; - channel.setMethodCallHandler(handleObserverCallbacks); - } - - /// Instructs the iOS implementation to register a transaction observer and - /// start listening to it. - /// - /// Call this method when the first listener is subscribed to the - /// [InAppPurchaseIosPlatform.purchaseStream]. - Future startObservingTransactionQueue() => channel - .invokeMethod('-[SKPaymentQueue startObservingTransactionQueue]'); - - /// Instructs the iOS implementation to remove the transaction observer and - /// stop listening to it. - /// - /// Call this when there are no longer any listeners subscribed to the - /// [InAppPurchaseIosPlatform.purchaseStream]. - Future stopObservingTransactionQueue() => channel - .invokeMethod('-[SKPaymentQueue stopObservingTransactionQueue]'); - - /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. - /// - /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to - /// finish transactions when the storefront changes or if the price consent - /// sheet should be displayed when the price of a subscription has changed. If - /// no delegate is registered iOS will fallback to it's default configuration. - /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). - /// - /// When set to `null` the payment queue delegate will be removed and the - /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). - Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) async { - if (delegate == null) { - await channel.invokeMethod('-[SKPaymentQueue removeDelegate]'); - paymentQueueDelegateChannel.setMethodCallHandler(null); - } else { - await channel.invokeMethod('-[SKPaymentQueue registerDelegate]'); - paymentQueueDelegateChannel - .setMethodCallHandler(handlePaymentQueueDelegateCallbacks); - } - - _paymentQueueDelegate = delegate; - } - - /// Posts a payment to the queue. - /// - /// This sends a purchase request to the App Store for confirmation. - /// Transaction updates will be delivered to the set - /// [SkTransactionObserverWrapper]. - /// - /// A couple preconditions need to be met before calling this method. - /// - /// - At least one [SKTransactionObserverWrapper] should have been added to - /// the payment queue using [addTransactionObserver]. - /// - The [payment.productIdentifier] needs to have been previously fetched - /// using [SKRequestMaker.startProductRequest] so that a valid `SKProduct` - /// has been cached in the platform side already. Because of this - /// [payment.productIdentifier] cannot be hardcoded. - /// - /// This method calls StoreKit's [`-[SKPaymentQueue addPayment:]`] - /// (https://developer.apple.com/documentation/storekit/skpaymentqueue/1506036-addpayment?preferredLanguage=occ). - /// - /// Also see [sandbox - /// testing](https://developer.apple.com/apple-pay/sandbox-testing/). - Future addPayment(SKPaymentWrapper payment) async { - assert(_observer != null, - '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); - final Map requestMap = payment.toMap(); - await channel.invokeMethod( - '-[InAppPurchasePlugin addPayment:result:]', - requestMap, - ); - } - - /// Finishes a transaction and removes it from the queue. - /// - /// This method should be called after the given [transaction] has been - /// succesfully processed and its content has been delivered to the user. - /// Transaction status updates are propagated to [SkTransactionObserver]. - /// - /// This will throw a Platform exception if [transaction.transactionState] is - /// [SKPaymentTransactionStateWrapper.purchasing]. - /// - /// This method calls StoreKit's [`-[SKPaymentQueue - /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). - Future finishTransaction( - SKPaymentTransactionWrapper transaction) async { - Map requestMap = transaction.toFinishMap(); - await channel.invokeMethod( - '-[InAppPurchasePlugin finishTransaction:result:]', - requestMap, - ); - } - - /// Restore previously purchased transactions. - /// - /// Use this to load previously purchased content on a new device. - /// - /// This call triggers purchase updates on the set - /// [SKTransactionObserverWrapper] for previously made transactions. This will - /// invoke [SKTransactionObserverWrapper.restoreCompletedTransactions], - /// [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished], - /// and [SKTransactionObserverWrapper.updatedTransaction]. These restored - /// transactions need to be marked complete with [finishTransaction] once the - /// content is delivered, like any other transaction. - /// - /// The `applicationUserName` should match the original - /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. - /// If no `applicationUserName` was used, `applicationUserName` should be null. - /// - /// This method either triggers [`-[SKPayment - /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) - /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) - /// depending on whether the `applicationUserName` is set. - Future restoreTransactions({String? applicationUserName}) async { - await channel.invokeMethod( - '-[InAppPurchasePlugin restoreTransactions:result:]', - applicationUserName); - } - - /// Present Code Redemption Sheet - /// - /// Use this to allow Users to enter and redeem Codes - /// - /// This method triggers [`-[SKPayment - /// presentCodeRedemptionSheet]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3566726-presentcoderedemptionsheet?language=objc) - Future presentCodeRedemptionSheet() async { - await channel.invokeMethod( - '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]'); - } - - /// Shows the price consent sheet if the user has not yet responded to a - /// subscription price change. - /// - /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] - /// (using the [setDelegate] method) and returned `false` when the - /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. - /// - /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). - Future showPriceConsentIfNeeded() async { - await channel - .invokeMethod('-[SKPaymentQueue showPriceConsentIfNeeded]'); - } - - /// Triage a method channel call from the platform and triggers the correct observer method. - /// - /// This method is public for testing purposes only and should not be used - /// outside this class. - @visibleForTesting - Future handleObserverCallbacks(MethodCall call) async { - assert(_observer != null, - '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); - final SKTransactionObserverWrapper observer = _observer!; - switch (call.method) { - case 'updatedTransactions': - { - final List transactions = - _getTransactionList(call.arguments); - return Future(() { - observer.updatedTransactions(transactions: transactions); - }); - } - case 'removedTransactions': - { - final List transactions = - _getTransactionList(call.arguments); - return Future(() { - observer.removedTransactions(transactions: transactions); - }); - } - case 'restoreCompletedTransactionsFailed': - { - SKError error = - SKError.fromJson(Map.from(call.arguments)); - return Future(() { - observer.restoreCompletedTransactionsFailed(error: error); - }); - } - case 'paymentQueueRestoreCompletedTransactionsFinished': - { - return Future(() { - observer.paymentQueueRestoreCompletedTransactionsFinished(); - }); - } - case 'shouldAddStorePayment': - { - SKPaymentWrapper payment = - SKPaymentWrapper.fromJson(call.arguments['payment']); - SKProductWrapper product = - SKProductWrapper.fromJson(call.arguments['product']); - return Future(() { - if (observer.shouldAddStorePayment( - payment: payment, product: product) == - true) { - SKPaymentQueueWrapper().addPayment(payment); - } - }); - } - default: - break; - } - throw PlatformException( - code: 'no_such_callback', - message: 'Did not recognize the observer callback ${call.method}.'); - } - - // Get transaction wrapper object list from arguments. - List _getTransactionList( - List transactionsData) { - return transactionsData.map((dynamic map) { - return SKPaymentTransactionWrapper.fromJson( - Map.castFrom(map)); - }).toList(); - } - - /// Triage a method channel call from the platform and triggers the correct - /// payment queue delegate method. - /// - /// This method is public for testing purposes only and should not be used - /// outside this class. - @visibleForTesting - Future handlePaymentQueueDelegateCallbacks(MethodCall call) async { - assert(_paymentQueueDelegate != null, - '[in_app_purchase]: (Fatal)The payment queue delegate has not been set but we received a payment queue notification. Please ensure the payment queue has been set using `setDelegate`.'); - - final SKPaymentQueueDelegateWrapper delegate = _paymentQueueDelegate!; - switch (call.method) { - case 'shouldContinueTransaction': - final SKPaymentTransactionWrapper transaction = - SKPaymentTransactionWrapper.fromJson(call.arguments['transaction']); - final SKStorefrontWrapper storefront = - SKStorefrontWrapper.fromJson(call.arguments['storefront']); - return delegate.shouldContinueTransaction(transaction, storefront); - case 'shouldShowPriceConsent': - return delegate.shouldShowPriceConsent(); - default: - break; - } - throw PlatformException( - code: 'no_such_callback', - message: - 'Did not recognize the payment queue delegate callback ${call.method}.'); - } -} - -/// Dart wrapper around StoreKit's -/// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). -@immutable -@JsonSerializable() -class SKError { - /// Creates a new [SKError] object with the provided information. - const SKError( - {required this.code, required this.domain, required this.userInfo}); - - /// Constructs an instance of this from a key-value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. The `map` parameter must not be - /// null. - factory SKError.fromJson(Map map) { - return _$SKErrorFromJson(map); - } - - /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) - /// as defined in the Cocoa Framework. - @JsonKey(defaultValue: 0) - final int code; - - /// Error - /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) - /// as defined in the Cocoa Framework. - @JsonKey(defaultValue: '') - final String domain; - - /// A map that contains more detailed information about the error. - /// - /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). - @JsonKey(defaultValue: {}) - final Map userInfo; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - final SKError typedOther = other as SKError; - return typedOther.code == code && - typedOther.domain == domain && - DeepCollectionEquality.unordered() - .equals(typedOther.userInfo, userInfo); - } - - @override - int get hashCode => hashValues( - code, - domain, - userInfo, - ); -} - -/// Dart wrapper around StoreKit's -/// [SKPayment](https://developer.apple.com/documentation/storekit/skpayment?language=objc). -/// -/// Used as the parameter to initiate a payment. In general, a developer should -/// not need to create the payment object explicitly; instead, use -/// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to -/// initiate a payment. -@immutable -@JsonSerializable() -class SKPaymentWrapper { - /// Creates a new [SKPaymentWrapper] with the provided information. - const SKPaymentWrapper( - {required this.productIdentifier, - this.applicationUsername, - this.requestData, - this.quantity = 1, - this.simulatesAskToBuyInSandbox = false}); - - /// Constructs an instance of this from a key value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. The `map` parameter must not be - /// null. - factory SKPaymentWrapper.fromJson(Map map) { - assert(map != null); - return _$SKPaymentWrapperFromJson(map); - } - - /// Creates a Map object describes the payment object. - Map toMap() { - return { - 'productIdentifier': productIdentifier, - 'applicationUsername': applicationUsername, - 'requestData': requestData, - 'quantity': quantity, - 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox - }; - } - - /// The id for the product that the payment is for. - @JsonKey(defaultValue: '') - final String productIdentifier; - - /// An opaque id for the user's account. - /// - /// Used to help the store detect irregular activity. See - /// [applicationUsername](https://developer.apple.com/documentation/storekit/skpayment/1506116-applicationusername?language=objc) - /// for more details. For example, you can use a one-way hash of the user’s - /// account name on your server. Don’t use the Apple ID for your developer - /// account, the user’s Apple ID, or the user’s plaintext account name on - /// your server. - final String? applicationUsername; - - /// Reserved for future use. - /// - /// The value must be null before sending the payment. If the value is not - /// null, the payment will be rejected. - /// - // The iOS Platform provided this property but it is reserved for future use. - // We also provide this property to match the iOS platform. Converted to - // String from NSData from ios platform using UTF8Encoding. The / default is - // null. - final String? requestData; - - /// The amount of the product this payment is for. - /// - /// The default is 1. The minimum is 1. The maximum is 10. - /// - /// If the object is invalid, the value could be 0. - @JsonKey(defaultValue: 0) - final int quantity; - - /// Produces an "ask to buy" flow in the sandbox. - /// - /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], - /// which produce an "ask to buy" prompt that interrupts the the payment flow. - /// - /// Default is `false`. - /// - /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox - /// testing. - @JsonKey(defaultValue: false) - final bool simulatesAskToBuyInSandbox; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - final SKPaymentWrapper typedOther = other as SKPaymentWrapper; - return typedOther.productIdentifier == productIdentifier && - typedOther.applicationUsername == applicationUsername && - typedOther.quantity == quantity && - typedOther.simulatesAskToBuyInSandbox == simulatesAskToBuyInSandbox && - typedOther.requestData == requestData; - } - - @override - int get hashCode => hashValues(productIdentifier, applicationUsername, - quantity, simulatesAskToBuyInSandbox, requestData); - - @override - String toString() => _$SKPaymentWrapperToJson(this).toString(); -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart deleted file mode 100644 index 2b886597adc5..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ /dev/null @@ -1,44 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'sk_payment_queue_wrapper.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SKError _$SKErrorFromJson(Map json) { - return SKError( - code: json['code'] as int? ?? 0, - domain: json['domain'] as String? ?? '', - userInfo: (json['userInfo'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - ) ?? - {}, - ); -} - -Map _$SKErrorToJson(SKError instance) => { - 'code': instance.code, - 'domain': instance.domain, - 'userInfo': instance.userInfo, - }; - -SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { - return SKPaymentWrapper( - productIdentifier: json['productIdentifier'] as String? ?? '', - applicationUsername: json['applicationUsername'] as String?, - requestData: json['requestData'] as String?, - quantity: json['quantity'] as int? ?? 0, - simulatesAskToBuyInSandbox: - json['simulatesAskToBuyInSandbox'] as bool? ?? false, - ); -} - -Map _$SKPaymentWrapperToJson(SKPaymentWrapper instance) => - { - 'productIdentifier': instance.productIdentifier, - 'applicationUsername': instance.applicationUsername, - 'requestData': instance.requestData, - 'quantity': instance.quantity, - 'simulatesAskToBuyInSandbox': instance.simulatesAskToBuyInSandbox, - }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart deleted file mode 100644 index 4c7af21bc151..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart +++ /dev/null @@ -1,37 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'sk_payment_transaction_wrappers.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) { - return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper.fromJson( - Map.from(json['payment'] as Map)), - transactionState: const SKTransactionStatusConverter() - .fromJson(json['transactionState'] as int?), - originalTransaction: json['originalTransaction'] == null - ? null - : SKPaymentTransactionWrapper.fromJson( - Map.from(json['originalTransaction'] as Map)), - transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), - transactionIdentifier: json['transactionIdentifier'] as String?, - error: json['error'] == null - ? null - : SKError.fromJson(Map.from(json['error'] as Map)), - ); -} - -Map _$SKPaymentTransactionWrapperToJson( - SKPaymentTransactionWrapper instance) => - { - 'transactionState': const SKTransactionStatusConverter() - .toJson(instance.transactionState), - 'payment': instance.payment, - 'originalTransaction': instance.originalTransaction, - 'transactionTimeStamp': instance.transactionTimeStamp, - 'transactionIdentifier': instance.transactionIdentifier, - 'error': instance.error, - }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart deleted file mode 100644 index 66f4b7827c38..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ /dev/null @@ -1,125 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'sk_product_wrapper.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) { - return SkProductResponseWrapper( - products: (json['products'] as List?) - ?.map((e) => - SKProductWrapper.fromJson(Map.from(e as Map))) - .toList() ?? - [], - invalidProductIdentifiers: - (json['invalidProductIdentifiers'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], - ); -} - -Map _$SkProductResponseWrapperToJson( - SkProductResponseWrapper instance) => - { - 'products': instance.products, - 'invalidProductIdentifiers': instance.invalidProductIdentifiers, - }; - -SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( - Map json) { - return SKProductSubscriptionPeriodWrapper( - numberOfUnits: json['numberOfUnits'] as int? ?? 0, - unit: const SKSubscriptionPeriodUnitConverter() - .fromJson(json['unit'] as int?), - ); -} - -Map _$SKProductSubscriptionPeriodWrapperToJson( - SKProductSubscriptionPeriodWrapper instance) => - { - 'numberOfUnits': instance.numberOfUnits, - 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), - }; - -SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { - return SKProductDiscountWrapper( - price: json['price'] as String? ?? '', - priceLocale: - SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, - paymentMode: const SKProductDiscountPaymentModeConverter() - .fromJson(json['paymentMode'] as int?), - subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( - (json['subscriptionPeriod'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - ); -} - -Map _$SKProductDiscountWrapperToJson( - SKProductDiscountWrapper instance) => - { - 'price': instance.price, - 'priceLocale': instance.priceLocale, - 'numberOfPeriods': instance.numberOfPeriods, - 'paymentMode': const SKProductDiscountPaymentModeConverter() - .toJson(instance.paymentMode), - 'subscriptionPeriod': instance.subscriptionPeriod, - }; - -SKProductWrapper _$SKProductWrapperFromJson(Map json) { - return SKProductWrapper( - productIdentifier: json['productIdentifier'] as String? ?? '', - localizedTitle: json['localizedTitle'] as String? ?? '', - localizedDescription: json['localizedDescription'] as String? ?? '', - priceLocale: - SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String?, - price: json['price'] as String? ?? '', - subscriptionPeriod: json['subscriptionPeriod'] == null - ? null - : SKProductSubscriptionPeriodWrapper.fromJson( - (json['subscriptionPeriod'] as Map?)?.map( - (k, e) => MapEntry(k as String, e), - )), - introductoryPrice: json['introductoryPrice'] == null - ? null - : SKProductDiscountWrapper.fromJson( - Map.from(json['introductoryPrice'] as Map)), - ); -} - -Map _$SKProductWrapperToJson(SKProductWrapper instance) => - { - 'productIdentifier': instance.productIdentifier, - 'localizedTitle': instance.localizedTitle, - 'localizedDescription': instance.localizedDescription, - 'priceLocale': instance.priceLocale, - 'subscriptionGroupIdentifier': instance.subscriptionGroupIdentifier, - 'price': instance.price, - 'subscriptionPeriod': instance.subscriptionPeriod, - 'introductoryPrice': instance.introductoryPrice, - }; - -SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) { - return SKPriceLocaleWrapper( - currencySymbol: json['currencySymbol'] as String? ?? '', - currencyCode: json['currencyCode'] as String? ?? '', - countryCode: json['countryCode'] as String? ?? '', - ); -} - -Map _$SKPriceLocaleWrapperToJson( - SKPriceLocaleWrapper instance) => - { - 'currencySymbol': instance.currencySymbol, - 'currencyCode': instance.currencyCode, - 'countryCode': instance.countryCode, - }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart deleted file mode 100644 index f75cfc5711e8..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'sk_storefront_wrapper.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SKStorefrontWrapper _$SKStorefrontWrapperFromJson(Map json) { - return SKStorefrontWrapper( - countryCode: json['countryCode'] as String, - identifier: json['identifier'] as String, - ); -} - -Map _$SKStorefrontWrapperToJson( - SKStorefrontWrapper instance) => - { - 'countryCode': instance.countryCode, - 'identifier': instance.identifier, - }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml deleted file mode 100644 index 9ba642e2e590..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: in_app_purchase_ios -description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. -repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+4 - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" - -flutter: - plugin: - implements: in_app_purchase - platforms: - ios: - pluginClass: InAppPurchasePlugin - -dependencies: - collection: ^1.15.0 - flutter: - sdk: flutter - in_app_purchase_platform_interface: ^1.1.0 - json_annotation: ^4.0.1 - meta: ^1.3.0 - -dev_dependencies: - build_runner: ^1.11.1 - flutter_test: - sdk: flutter - json_serializable: ^4.1.1 - test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart deleted file mode 100644 index e7dbd1a49ae2..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_ios/src/channel.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; - -import '../store_kit_wrappers/sk_test_stub_objects.dart'; - -class FakeIOSPlatform { - FakeIOSPlatform() { - channel.setMockMethodCallHandler(onMethodCall); - } - - // pre-configured store informations - String? receiptData; - late Set validProductIDs; - late Map validProducts; - late List transactions; - late List finishedTransactions; - late bool testRestoredTransactionsNull; - late bool testTransactionFail; - PlatformException? queryProductException; - PlatformException? restoreException; - SKError? testRestoredError; - bool queueIsActive = false; - - void reset() { - transactions = []; - receiptData = 'dummy base64data'; - validProductIDs = ['123', '456'].toSet(); - validProducts = Map(); - for (String validID in validProductIDs) { - Map productWrapperMap = - buildProductMap(dummyProductWrapper); - productWrapperMap['productIdentifier'] = validID; - if (validID == '456') { - productWrapperMap['priceLocale'] = buildLocaleMap(noSymbolLocale); - } - validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); - } - - SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper( - transactionIdentifier: '123', - payment: dummyPayment, - originalTransaction: dummyTransaction, - transactionTimeStamp: 123123123.022, - transactionState: SKPaymentTransactionStateWrapper.restored, - error: null, - ); - SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper( - transactionIdentifier: '1234', - payment: dummyPayment, - originalTransaction: dummyTransaction, - transactionTimeStamp: 123123123.022, - transactionState: SKPaymentTransactionStateWrapper.restored, - error: null, - ); - - transactions.addAll([tran1, tran2]); - finishedTransactions = []; - testRestoredTransactionsNull = false; - testTransactionFail = false; - queryProductException = null; - restoreException = null; - testRestoredError = null; - queueIsActive = false; - } - - SKPaymentTransactionWrapper createPendingTransaction(String id) { - return SKPaymentTransactionWrapper( - transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchasing, - transactionTimeStamp: 123123.121, - error: null, - originalTransaction: null); - } - - SKPaymentTransactionWrapper createPurchasedTransaction( - String productId, String transactionId) { - return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: productId), - transactionState: SKPaymentTransactionStateWrapper.purchased, - transactionTimeStamp: 123123.121, - transactionIdentifier: transactionId, - error: null, - originalTransaction: null); - } - - SKPaymentTransactionWrapper createFailedTransaction(String productId) { - return SKPaymentTransactionWrapper( - transactionIdentifier: '', - payment: SKPaymentWrapper(productIdentifier: productId), - transactionState: SKPaymentTransactionStateWrapper.failed, - transactionTimeStamp: 123123.121, - error: SKError( - code: 0, - domain: 'ios_domain', - userInfo: {'message': 'an error message'}), - originalTransaction: null); - } - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case '-[SKPaymentQueue canMakePayments:]': - return Future.value(true); - case '-[InAppPurchasePlugin startProductRequest:result:]': - if (queryProductException != null) { - throw queryProductException!; - } - List productIDS = - List.castFrom(call.arguments); - List invalidFound = []; - List products = []; - for (String productID in productIDS) { - if (!validProductIDs.contains(productID)) { - invalidFound.add(productID); - } else { - products.add(validProducts[productID]!); - } - } - SkProductResponseWrapper response = SkProductResponseWrapper( - products: products, invalidProductIdentifiers: invalidFound); - return Future>.value( - buildProductResponseMap(response)); - case '-[InAppPurchasePlugin restoreTransactions:result:]': - if (restoreException != null) { - throw restoreException!; - } - if (testRestoredError != null) { - InAppPurchaseIosPlatform.observer - .restoreCompletedTransactionsFailed(error: testRestoredError!); - return Future.sync(() {}); - } - if (!testRestoredTransactionsNull) { - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: transactions); - } - InAppPurchaseIosPlatform.observer - .paymentQueueRestoreCompletedTransactionsFinished(); - - return Future.sync(() {}); - case '-[InAppPurchasePlugin retrieveReceiptData:result:]': - if (receiptData != null) { - return Future.value(receiptData); - } else { - throw PlatformException(code: 'no_receipt_data'); - } - case '-[InAppPurchasePlugin refreshReceipt:result:]': - receiptData = 'refreshed receipt data'; - return Future.sync(() {}); - case '-[InAppPurchasePlugin addPayment:result:]': - String id = call.arguments['productIdentifier']; - SKPaymentTransactionWrapper transaction = createPendingTransaction(id); - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: [transaction]); - sleep(const Duration(milliseconds: 30)); - if (testTransactionFail) { - SKPaymentTransactionWrapper transaction_failed = - createFailedTransaction(id); - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: [transaction_failed]); - } else { - SKPaymentTransactionWrapper transaction_finished = - createPurchasedTransaction( - id, transaction.transactionIdentifier ?? ''); - InAppPurchaseIosPlatform.observer - .updatedTransactions(transactions: [transaction_finished]); - } - break; - case '-[InAppPurchasePlugin finishTransaction:result:]': - finishedTransactions.add(createPurchasedTransaction( - call.arguments["productIdentifier"], - call.arguments["transactionIdentifier"])); - break; - case '-[SKPaymentQueue startObservingTransactionQueue]': - queueIsActive = true; - break; - case '-[SKPaymentQueue stopObservingTransactionQueue]': - queueIsActive = false; - break; - } - return Future.sync(() {}); - } -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart deleted file mode 100644 index f8b75195fc6e..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart +++ /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 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; - -import 'fakes/fake_ios_platform.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); - - setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); - }); - - group('present code redemption sheet', () { - test('null', () async { - expect( - await InAppPurchaseIosPlatformAddition().presentCodeRedemptionSheet(), - null); - }); - }); - - group('refresh receipt data', () { - test('should refresh receipt data', () async { - PurchaseVerificationData? receiptData = - await InAppPurchaseIosPlatformAddition() - .refreshPurchaseVerificationData(); - expect(receiptData, isNotNull); - expect(receiptData!.source, kIAPSource); - expect(receiptData.localVerificationData, 'refreshed receipt data'); - expect(receiptData.serverVerificationData, 'refreshed receipt data'); - }); - }); -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart deleted file mode 100644 index 865468f532bf..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; -import 'package:in_app_purchase_ios/src/store_kit_wrappers/enum_converters.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; -import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; - -import 'fakes/fake_ios_platform.dart'; -import 'store_kit_wrappers/sk_test_stub_objects.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); - late InAppPurchaseIosPlatform iapIosPlatform; - - setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); - }); - - setUp(() { - InAppPurchaseIosPlatform.registerPlatform(); - iapIosPlatform = InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; - fakeIOSPlatform.reset(); - }); - - tearDown(() => fakeIOSPlatform.reset()); - - group('isAvailable', () { - test('true', () async { - expect(await iapIosPlatform.isAvailable(), isTrue); - }); - }); - - group('query product list', () { - test('should get product list and correct invalid identifiers', () async { - final InAppPurchaseIosPlatform connection = InAppPurchaseIosPlatform(); - final ProductDetailsResponse response = await connection - .queryProductDetails(['123', '456', '789'].toSet()); - List products = response.productDetails; - expect(products.first.id, '123'); - expect(products[1].id, '456'); - expect(response.notFoundIDs, ['789']); - expect(response.error, isNull); - expect(response.productDetails.first.currencySymbol, r'$'); - expect(response.productDetails[1].currencySymbol, 'EUR'); - }); - - test( - 'if query products throws error, should get error object in the response', - () async { - fakeIOSPlatform.queryProductException = PlatformException( - code: 'error_code', - message: 'error_message', - details: {'info': 'error_info'}); - final InAppPurchaseIosPlatform connection = InAppPurchaseIosPlatform(); - final ProductDetailsResponse response = await connection - .queryProductDetails(['123', '456', '789'].toSet()); - expect(response.productDetails, []); - expect(response.notFoundIDs, ['123', '456', '789']); - expect(response.error, isNotNull); - expect(response.error!.source, kIAPSource); - expect(response.error!.code, 'error_code'); - expect(response.error!.message, 'error_message'); - expect(response.error!.details, {'info': 'error_info'}); - }); - }); - - group('restore purchases', () { - test('should emit restored transactions on purchase stream', () async { - Completer completer = Completer(); - Stream> stream = iapIosPlatform.purchaseStream; - - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - if (purchaseDetailsList.first.status == PurchaseStatus.restored) { - completer.complete(purchaseDetailsList); - subscription.cancel(); - } - }); - - await iapIosPlatform.restorePurchases(); - List details = await completer.future; - - expect(details.length, 2); - for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { - SKPaymentTransactionWrapper expected = fakeIOSPlatform.transactions[i]; - PurchaseDetails actual = details[i]; - - expect(actual.purchaseID, expected.transactionIdentifier); - expect(actual.verificationData, isNotNull); - expect(actual.status, PurchaseStatus.restored); - expect(actual.verificationData.localVerificationData, - fakeIOSPlatform.receiptData); - expect(actual.verificationData.serverVerificationData, - fakeIOSPlatform.receiptData); - expect(actual.pendingCompletePurchase, true); - } - }); - - test('should not block transaction updates', () async { - fakeIOSPlatform.transactions - .insert(0, fakeIOSPlatform.createPurchasedTransaction('foo', 'bar')); - Completer completer = Completer(); - Stream> stream = iapIosPlatform.purchaseStream; - - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { - completer.complete(purchaseDetailsList); - subscription.cancel(); - } - }); - await iapIosPlatform.restorePurchases(); - List details = await completer.future; - expect(details.length, 3); - for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { - SKPaymentTransactionWrapper expected = fakeIOSPlatform.transactions[i]; - PurchaseDetails actual = details[i]; - - expect(actual.purchaseID, expected.transactionIdentifier); - expect(actual.verificationData, isNotNull); - expect( - actual.status, - SKTransactionStatusConverter() - .toPurchaseStatus(expected.transactionState), - ); - expect(actual.verificationData.localVerificationData, - fakeIOSPlatform.receiptData); - expect(actual.verificationData.serverVerificationData, - fakeIOSPlatform.receiptData); - expect(actual.pendingCompletePurchase, true); - } - }); - - test('receipt error should populate null to verificationData.data', - () async { - fakeIOSPlatform.receiptData = null; - Completer completer = Completer(); - Stream> stream = iapIosPlatform.purchaseStream; - - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - if (purchaseDetailsList.first.status == PurchaseStatus.restored) { - completer.complete(purchaseDetailsList); - subscription.cancel(); - } - }); - - await iapIosPlatform.restorePurchases(); - List details = await completer.future; - - for (PurchaseDetails purchase in details) { - expect(purchase.verificationData.localVerificationData, isEmpty); - expect(purchase.verificationData.serverVerificationData, isEmpty); - } - }); - - test('test restore error', () { - fakeIOSPlatform.testRestoredError = SKError( - code: 123, - domain: 'error_test', - userInfo: {'message': 'errorMessage'}); - - expect( - () => iapIosPlatform.restorePurchases(), - throwsA( - isA() - .having((error) => error.code, 'code', 123) - .having((error) => error.domain, 'domain', 'error_test') - .having((error) => error.userInfo, 'userInfo', - {'message': 'errorMessage'}), - )); - }); - }); - - group('make payment', () { - test( - 'buying non consumable, should get purchase objects in the purchase update callback', - () async { - List details = []; - Completer completer = Completer(); - Stream> stream = iapIosPlatform.purchaseStream; - - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { - completer.complete(details); - subscription.cancel(); - } - }); - final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( - productDetails: - AppStoreProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); - - List result = await completer.future; - expect(result.length, 2); - expect(result.first.productID, dummyProductWrapper.productIdentifier); - }); - - test( - 'buying consumable, should get purchase objects in the purchase update callback', - () async { - List details = []; - Completer completer = Completer(); - Stream> stream = iapIosPlatform.purchaseStream; - - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { - completer.complete(details); - subscription.cancel(); - } - }); - final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( - productDetails: - AppStoreProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await iapIosPlatform.buyConsumable(purchaseParam: purchaseParam); - - List result = await completer.future; - expect(result.length, 2); - expect(result.first.productID, dummyProductWrapper.productIdentifier); - }); - - test('buying consumable, should throw when autoConsume is false', () async { - final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( - productDetails: - AppStoreProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - expect( - () => iapIosPlatform.buyConsumable( - purchaseParam: purchaseParam, autoConsume: false), - throwsA(isInstanceOf())); - }); - - test('should get failed purchase status', () async { - fakeIOSPlatform.testTransactionFail = true; - List details = []; - Completer completer = Completer(); - late IAPError error; - - Stream> stream = iapIosPlatform.purchaseStream; - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - purchaseDetailsList.forEach((purchaseDetails) { - if (purchaseDetails.status == PurchaseStatus.error) { - error = purchaseDetails.error!; - completer.complete(error); - subscription.cancel(); - } - }); - }); - final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( - productDetails: - AppStoreProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); - - IAPError completerError = await completer.future; - expect(completerError.code, 'purchase_error'); - expect(completerError.source, kIAPSource); - expect(completerError.message, 'ios_domain'); - expect(completerError.details, {'message': 'an error message'}); - }); - }); - - group('complete purchase', () { - test('should complete purchase', () async { - List details = []; - Completer completer = Completer(); - Stream> stream = iapIosPlatform.purchaseStream; - late StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - purchaseDetailsList.forEach((purchaseDetails) { - if (purchaseDetails.pendingCompletePurchase) { - iapIosPlatform.completePurchase(purchaseDetails); - completer.complete(details); - subscription.cancel(); - } - }); - }); - final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( - productDetails: - AppStoreProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); - List result = await completer.future; - expect(result.length, 2); - expect(result.first.productID, dummyProductWrapper.productIdentifier); - expect(fakeIOSPlatform.finishedTransactions.length, 1); - }); - }); - - group('purchase stream', () { - test('Should only have active queue when purchaseStream has listeners', () { - Stream> stream = iapIosPlatform.purchaseStream; - expect(fakeIOSPlatform.queueIsActive, false); - StreamSubscription subscription1 = stream.listen((event) {}); - expect(fakeIOSPlatform.queueIsActive, true); - StreamSubscription subscription2 = stream.listen((event) {}); - expect(fakeIOSPlatform.queueIsActive, true); - subscription1.cancel(); - expect(fakeIOSPlatform.queueIsActive, true); - subscription2.cancel(); - expect(fakeIOSPlatform.queueIsActive, false); - }); - }); -} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart deleted file mode 100644 index c7f7d800f45f..000000000000 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ /dev/null @@ -1,298 +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:in_app_purchase_ios/src/channel.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; -import 'sk_test_stub_objects.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); - - setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); - }); - - setUp(() {}); - - tearDown(() { - fakeIOSPlatform.testReturnNull = false; - fakeIOSPlatform.queueIsActive = null; - fakeIOSPlatform.getReceiptFailTest = false; - }); - - group('sk_request_maker', () { - test('get products method channel', () async { - SkProductResponseWrapper productResponseWrapper = - await SKRequestMaker().startProductRequest(['xxx']); - expect( - productResponseWrapper.products, - isNotEmpty, - ); - expect( - productResponseWrapper.products.first.priceLocale.currencySymbol, - '\$', - ); - - expect( - productResponseWrapper.products.first.priceLocale.currencySymbol, - isNot('A'), - ); - expect( - productResponseWrapper.products.first.priceLocale.currencyCode, - 'USD', - ); - expect( - productResponseWrapper.products.first.priceLocale.countryCode, - 'US', - ); - expect( - productResponseWrapper.invalidProductIdentifiers, - isNotEmpty, - ); - - expect( - fakeIOSPlatform.startProductRequestParam, - ['xxx'], - ); - }); - - test('get products method channel should throw exception', () async { - fakeIOSPlatform.getProductRequestFailTest = true; - expect( - SKRequestMaker().startProductRequest(['xxx']), - throwsException, - ); - fakeIOSPlatform.getProductRequestFailTest = false; - }); - - test('refreshed receipt', () async { - int receiptCountBefore = fakeIOSPlatform.refreshReceipt; - await SKRequestMaker().startRefreshReceiptRequest( - receiptProperties: {"isExpired": true}); - expect(fakeIOSPlatform.refreshReceipt, receiptCountBefore + 1); - expect(fakeIOSPlatform.refreshReceiptParam, - {"isExpired": true}); - }); - - test('should get null receipt if any exceptions are raised', () async { - fakeIOSPlatform.getReceiptFailTest = true; - expect(() async => SKReceiptManager.retrieveReceiptData(), - throwsA(TypeMatcher())); - }); - }); - - group('sk_receipt_manager', () { - test('should get receipt (faking it by returning a `receipt data` string)', - () async { - String receiptData = await SKReceiptManager.retrieveReceiptData(); - expect(receiptData, 'receipt data'); - }); - }); - - group('sk_payment_queue', () { - test('canMakePayment should return true', () async { - expect(await SKPaymentQueueWrapper.canMakePayments(), true); - }); - - test('canMakePayment returns false if method channel returns null', - () async { - fakeIOSPlatform.testReturnNull = true; - expect(await SKPaymentQueueWrapper.canMakePayments(), false); - }); - - test('transactions should return a valid list of transactions', () async { - expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); - }); - - test( - 'throws if observer is not set for payment queue before adding payment', - () async { - expect(SKPaymentQueueWrapper().addPayment(dummyPayment), - throwsAssertionError); - }); - - test('should add payment to the payment queue', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestPaymentTransactionObserver observer = - TestPaymentTransactionObserver(); - queue.setTransactionObserver(observer); - await queue.addPayment(dummyPayment); - expect(fakeIOSPlatform.payments.first, equals(dummyPayment)); - }); - - test('should finish transaction', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestPaymentTransactionObserver observer = - TestPaymentTransactionObserver(); - queue.setTransactionObserver(observer); - await queue.finishTransaction(dummyTransaction); - expect(fakeIOSPlatform.transactionsFinished.first, - equals(dummyTransaction.toFinishMap())); - }); - - test('should restore transaction', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestPaymentTransactionObserver observer = - TestPaymentTransactionObserver(); - queue.setTransactionObserver(observer); - await queue.restoreTransactions(applicationUserName: 'aUserID'); - expect(fakeIOSPlatform.applicationNameHasTransactionRestored, 'aUserID'); - }); - - test('startObservingTransactionQueue should call methodChannel', () async { - expect(fakeIOSPlatform.queueIsActive, isNot(true)); - await SKPaymentQueueWrapper().startObservingTransactionQueue(); - expect(fakeIOSPlatform.queueIsActive, true); - }); - - test('stopObservingTransactionQueue should call methodChannel', () async { - expect(fakeIOSPlatform.queueIsActive, isNot(false)); - await SKPaymentQueueWrapper().stopObservingTransactionQueue(); - expect(fakeIOSPlatform.queueIsActive, false); - }); - - test('setDelegate should call methodChannel', () async { - expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, false); - await SKPaymentQueueWrapper().setDelegate(TestPaymentQueueDelegate()); - expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, true); - await SKPaymentQueueWrapper().setDelegate(null); - expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, false); - }); - - test('showPriceConsentIfNeeded should call methodChannel', () async { - expect(fakeIOSPlatform.showPriceConsentIfNeeded, false); - await SKPaymentQueueWrapper().showPriceConsentIfNeeded(); - expect(fakeIOSPlatform.showPriceConsentIfNeeded, true); - }); - }); - - group('Code Redemption Sheet', () { - test('presentCodeRedemptionSheet should not throw', () async { - expect(fakeIOSPlatform.presentCodeRedemption, false); - await SKPaymentQueueWrapper().presentCodeRedemptionSheet(); - expect(fakeIOSPlatform.presentCodeRedemption, true); - fakeIOSPlatform.presentCodeRedemption = false; - }); - }); -} - -class FakeIOSPlatform { - FakeIOSPlatform() { - channel.setMockMethodCallHandler(onMethodCall); - } - // get product request - List startProductRequestParam = []; - bool getProductRequestFailTest = false; - bool testReturnNull = false; - - // get receipt request - bool getReceiptFailTest = false; - - // refresh receipt request - int refreshReceipt = 0; - late Map refreshReceiptParam; - - // payment queue - List payments = []; - List> transactionsFinished = []; - String applicationNameHasTransactionRestored = ''; - - // present Code Redemption - bool presentCodeRedemption = false; - - // show price consent sheet - bool showPriceConsentIfNeeded = false; - - // indicate if the payment queue delegate is registered - bool isPaymentQueueDelegateRegistered = false; - - // Listen to purchase updates - bool? queueIsActive; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - // request makers - case '-[InAppPurchasePlugin startProductRequest:result:]': - startProductRequestParam = call.arguments; - if (getProductRequestFailTest) { - return Future.value(null); - } - return Future>.value( - buildProductResponseMap(dummyProductResponseWrapper)); - case '-[InAppPurchasePlugin refreshReceipt:result:]': - refreshReceipt++; - refreshReceiptParam = - Map.castFrom(call.arguments); - return Future.sync(() {}); - // receipt manager - case '-[InAppPurchasePlugin retrieveReceiptData:result:]': - if (getReceiptFailTest) { - throw ("some arbitrary error"); - } - return Future.value('receipt data'); - // payment queue - case '-[SKPaymentQueue canMakePayments:]': - if (testReturnNull) { - return Future.value(null); - } - return Future.value(true); - case '-[SKPaymentQueue transactions]': - return Future>.value( - [buildTransactionMap(dummyTransaction)]); - case '-[InAppPurchasePlugin addPayment:result:]': - payments.add(SKPaymentWrapper.fromJson( - Map.from(call.arguments))); - return Future.sync(() {}); - case '-[InAppPurchasePlugin finishTransaction:result:]': - transactionsFinished.add(Map.from(call.arguments)); - return Future.sync(() {}); - case '-[InAppPurchasePlugin restoreTransactions:result:]': - applicationNameHasTransactionRestored = call.arguments; - return Future.sync(() {}); - case '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]': - presentCodeRedemption = true; - return Future.sync(() {}); - case '-[SKPaymentQueue startObservingTransactionQueue]': - queueIsActive = true; - return Future.sync(() {}); - case '-[SKPaymentQueue stopObservingTransactionQueue]': - queueIsActive = false; - return Future.sync(() {}); - case '-[SKPaymentQueue registerDelegate]': - isPaymentQueueDelegateRegistered = true; - return Future.sync(() {}); - case '-[SKPaymentQueue removeDelegate]': - isPaymentQueueDelegateRegistered = false; - return Future.sync(() {}); - case '-[SKPaymentQueue showPriceConsentIfNeeded]': - showPriceConsentIfNeeded = true; - return Future.sync(() {}); - } - return Future.error('method not mocked'); - } -} - -class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {} - -class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { - void updatedTransactions( - {required List transactions}) {} - - void removedTransactions( - {required List transactions}) {} - - void restoreCompletedTransactionsFailed({required SKError error}) {} - - void paymentQueueRestoreCompletedTransactionsFinished() {} - - bool shouldAddStorePayment( - {required SKPaymentWrapper payment, required SKProductWrapper product}) { - return true; - } -} 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 cd4b86d7f39a..f7d3268d1cae 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,15 @@ +## NEXT + +* Removes unnecessary imports. + +## 1.3.1 + +* Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. + +## 1.3.0 + +* Added new `PurchaseStatus` named `canceled` to distinguish between an error and user cancellation. + ## 1.2.0 * Added `toString()` to `IAPError` @@ -12,4 +24,4 @@ ## 1.0.0 -* Initial open-source release. \ No newline at end of file +* Initial open-source release. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart index eac4a0712078..d62e8e5f39b0 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.dart @@ -31,7 +31,7 @@ abstract class InAppPurchasePlatform extends PlatformInterface { // TODO(amirh): Extract common platform interface logic. // https://github.com/flutter/flutter/issues/43368 static set instance(InAppPurchasePlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart index 78695066702d..ed54e97442a2 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.dart @@ -26,4 +26,9 @@ enum PurchaseStatus { /// the purchase by calling the `completePurchase` method. More information on /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#restoring-previous-purchases). restored, + + /// The purchase has been canceled. + /// + /// Update your UI to indicate the purchase is canceled. + canceled, } 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 64574e0cf306..98698b80718c 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 @@ -1,19 +1,19 @@ name: in_app_purchase_platform_interface description: A common platform interface for the in_app_purchase plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_platform_interface 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.2.0 +version: 1.3.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: 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 ce49d9992131..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 @@ -27,4 +27,22 @@ void main() { expect(productDetails.currencySymbol, r'$'); }); }); + + group('PurchaseStatus Tests', () { + test('PurchaseStatus should contain 5 options', () { + const List values = PurchaseStatus.values; + + expect(values.length, 5); + }); + + test('PurchaseStatus enum should have items in correct index', () { + const List values = PurchaseStatus.values; + + expect(values[0], PurchaseStatus.pending); + expect(values[1], PurchaseStatus.purchased); + expect(values[2], PurchaseStatus.error); + expect(values[3], PurchaseStatus.restored); + expect(values[4], PurchaseStatus.canceled); + }); + }); } diff --git a/packages/in_app_purchase/in_app_purchase_ios/AUTHORS b/packages/in_app_purchase/in_app_purchase_storekit/AUTHORS similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/AUTHORS rename to packages/in_app_purchase/in_app_purchase_storekit/AUTHORS diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md new file mode 100644 index 000000000000..445fdb6aa491 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -0,0 +1,54 @@ +## 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. + +## 0.3.0+1 + +* Removes dependency on `meta`. + +## 0.3.0 + +* **BREAKING CHANGE:** `InAppPurchaseStoreKitPlatform.restorePurchase()` emits an empty instance of `List` when there were no transactions to restore, indicating that the restore procedure has finished. + +## 0.2.1 + +* Renames `in_app_purchase_ios` to `in_app_purchase_storekit` to facilitate + future macOS support. diff --git a/packages/device_info/device_info/LICENSE b/packages/in_app_purchase/in_app_purchase_storekit/LICENSE similarity index 100% rename from packages/device_info/device_info/LICENSE rename to packages/in_app_purchase/in_app_purchase_storekit/LICENSE diff --git a/packages/in_app_purchase/in_app_purchase_storekit/README.md b/packages/in_app_purchase/in_app_purchase_storekit/README.md new file mode 100644 index 000000000000..76e2854c26e1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/README.md @@ -0,0 +1,29 @@ +# in\_app\_purchase\_storekit + +The iOS implementation of [`in_app_purchase`][1]. + +## Usage + +This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. This package will be automatically included in your app +when you do. + +If you wish to use this package only, you can [add `in_app_purchase_storekit` directly][3]. + +## Contributing + +This plugin uses +[json_serializable](https://pub.dev/packages/json_serializable) for the +many data structs passed between the underlying platform layers and Dart. After +editing any of the serialized data structs, rebuild the serializers by running +`flutter packages pub run build_runner build --delete-conflicting-outputs`. +`flutter packages pub run build_runner watch --delete-conflicting-outputs` will +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/main/CONTRIBUTING.md). + + +[1]: ../in_app_purchase +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/in_app_purchase_storekit/install diff --git a/packages/in_app_purchase/in_app_purchase_storekit/build.yaml b/packages/in_app_purchase/in_app_purchase_storekit/build.yaml new file mode 100644 index 000000000000..651a557fc1ca --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/build.yaml @@ -0,0 +1,8 @@ +# See https://pub.dev/packages/build_config +targets: + $default: + builders: + json_serializable: + options: + any_map: true + create_to_json: false diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/README.md b/packages/in_app_purchase/in_app_purchase_storekit/example/README.md similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/README.md rename to packages/in_app_purchase/in_app_purchase_storekit/example/README.md diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/integration_test/in_app_purchase_test.dart similarity index 76% rename from packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart rename to packages/in_app_purchase/in_app_purchase_storekit/example/integration_test/in_app_purchase_test.dart index 3d68d0f1f4f0..32ea11314ee8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/integration_test/in_app_purchase_test.dart @@ -3,16 +3,16 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase_ios/in_app_purchase_ios.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:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Can create InAppPurchaseAndroid instance', + testWidgets('Can create InAppPurchaseStoreKit instance', (WidgetTester tester) async { - InAppPurchaseIosPlatform.registerPlatform(); + InAppPurchaseStoreKitPlatform.registerPlatform(); final InAppPurchasePlatform androidPlatform = InAppPurchasePlatform.instance; expect(androidPlatform, isNotNull); diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/battery/battery/example/ios/Flutter/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/battery/battery/example/ios/Flutter/Debug.xcconfig rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/Debug.xcconfig diff --git a/packages/battery/battery/example/ios/Flutter/Release.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/battery/battery/example/ios/Flutter/Release.xcconfig rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/Release.xcconfig diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Podfile rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile 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 new file mode 100644 index 000000000000..3977d549af12 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,680 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1630769A874F9381BC761FE1 /* libPods-Runner.a */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 688DE35021F2A5A100EA2684 /* TranslatorTests.m */; }; + 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */; }; + 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34B21EEB4B800D37AEF /* Stubs.m */; }; + 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18D02AB334F1C07BB9A4374A /* 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 */; }; + 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 */ + +/* Begin PBXContainerItemProxy section */ + A59001A921E69658004A3E5E /* 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 */ + 10B860DFD91A1DF639D7BE1D /* 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 = ""; }; + 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 = ""; }; + 1630769A874F9381BC761FE1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2550EB3A5A3E749A54ADCA2D /* 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 688DE35021F2A5A100EA2684 /* TranslatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TranslatorTests.m; sourceTree = ""; }; + 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ProductRequestHandlerTests.m; sourceTree = ""; }; + 6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stubs.h; sourceTree = ""; }; + 6896B34B21EEB4B800D37AEF /* Stubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Stubs.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 = ""; }; + 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 = ""; }; + 9D681E092EB0D20D652F69FC /* 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 = ""; }; + A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + A59001A421E69658004A3E5E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppPurchasePluginTests.m; sourceTree = ""; }; + 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 */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, + 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A121E69658004A3E5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0B4403AC68C3196AECF5EF89 /* Pods */ = { + isa = PBXGroup; + children = ( + E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */, + 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */, + 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */, + 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 334733E826680E5900DCC49E /* Temp */ = { + isa = PBXGroup; + children = ( + ); + path = Temp; + 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 = ( + 334733E826680E5900DCC49E /* Temp */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + A59001A521E69658004A3E5E /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + E4DB99639FAD8ADED6B572FC /* Frameworks */, + 0B4403AC68C3196AECF5EF89 /* Pods */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + A59001A421E69658004A3E5E /* 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 */, + F6E5D5F926131C4800C68BED /* Configuration.storekit */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A59001A521E69658004A3E5E /* RunnerTests */ = { + isa = PBXGroup; + children = ( + A59001A821E69658004A3E5E /* Info.plist */, + 6896B34A21EEB4B800D37AEF /* Stubs.h */, + 6896B34B21EEB4B800D37AEF /* Stubs.m */, + A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */, + 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */, + F78AF3132342BC89008449C7 /* PaymentQueueTests.m */, + 688DE35021F2A5A100EA2684 /* TranslatorTests.m */, + F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */, + F6995BDC27CF73000050EA78 /* FIATransactionCacheTests.m */, + ); + path = RunnerTests; + sourceTree = ""; + }; + E4DB99639FAD8ADED6B572FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5279297219369C600FF69E6 /* StoreKit.framework */, + 1630769A874F9381BC761FE1 /* libPods-Runner.a */, + 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + EDD921296E29F853F7B69716 /* [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"; + }; + A59001A321E69658004A3E5E /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */, + A59001A021E69658004A3E5E /* Sources */, + A59001A121E69658004A3E5E /* Frameworks */, + A59001A221E69658004A3E5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A59001AA21E69658004A3E5E /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = A59001A421E69658004A3E5E /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.InAppPurchase = { + enabled = 1; + }; + }; + }; + A59001A321E69658004A3E5E = { + CreatedOnToolsVersion = 10.0; + 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 */, + A59001A321E69658004A3E5E /* 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; + }; + A59001A221E69658004A3E5E /* 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"; + }; + 95C7A5986B77A8DF76F6DF3A /* [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"; + }; + EDD921296E29F853F7B69716 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A021E69658004A3E5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */, + 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 */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A59001AA21E69658004A3E5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = A59001A921E69658004A3E5E /* 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; + CURRENT_PROJECT_VERSION = 1; + 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.inAppPurchaseExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + 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.inAppPurchaseExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + A59001AB21E69658004A3E5E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = RunnerTests/Info.plist; + 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.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + A59001AC21E69658004A3E5E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + 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; + }; +/* 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; + }; + A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A59001AB21E69658004A3E5E /* Debug */, + A59001AC21E69658004A3E5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 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 new file mode 100644 index 000000000000..a8adf88572cd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/AppDelegate.h diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/AppDelegate.m diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Configuration.storekit similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Configuration.storekit diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/main.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/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/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m similarity index 99% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m index 810e1fafe11a..ea8787f55a0a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -8,7 +8,7 @@ #import "FIAPaymentQueueHandler.h" #import "Stubs.h" -@import in_app_purchase_ios; +@import in_app_purchase_storekit; API_AVAILABLE(ios(13.0)) @interface FIAPPaymentQueueDelegateTests : XCTestCase diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m new file mode 100644 index 000000000000..1ba0aea76e39 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m @@ -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 + +@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 new file mode 100644 index 000000000000..c89589c6a9e5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -0,0 +1,520 @@ +// Copyright 2013 The Flutter Authors. 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 "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@import in_app_purchase_storekit; + +@interface InAppPurchasePluginTest : XCTestCase + +@property(strong, nonatomic) FIAPReceiptManagerStub *receiptManagerStub; +@property(strong, nonatomic) InAppPurchasePlugin *plugin; + +@end + +@implementation InAppPurchasePluginTest + +- (void)setUp { + self.receiptManagerStub = [FIAPReceiptManagerStub new]; + self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; +} + +- (void)tearDown { +} + +- (void)testInvalidMethodCall { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect result to be not implemented"]; + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, FlutterMethodNotImplemented); +} + +- (void)testCanMakePayments { + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result to be YES"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" + arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, @YES); +} + +- (void)testGetProductResponse { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect response contains 1 item"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssert([result isKindOfClass:[NSDictionary class]]); + NSArray *resultArray = [result objectForKey:@"products"]; + XCTAssertEqual(resultArray.count, 1); + XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); +} + +- (void)testAddPaymentShouldReturnFlutterErrorWhenArgumentsAreInvalid { + XCTestExpectation *expectation = + [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:argument]; + [self.plugin handleMethodCall:call + 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]; +} + +- (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails { + NSDictionary *arguments = @{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return failed state."]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:arguments]; + + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(NO); + self.plugin.paymentQueueHandler = mockHandler; + + [self.plugin handleMethodCall:call + 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]; + OCMVerify(times(1), [mockHandler addPayment:[OCMArg any]]); +} + +- (void)testAddPaymentSuccessWithoutPaymentDiscount { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return success state"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; + 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]; +} + +- (void)testAddPaymentSuccessWithPaymentDiscount { + XCTestExpectation *expectation = + [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), + } + }]; + + 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"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : [NSNull null], + }]; + 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; + return !payment.simulatesAskToBuyInSandbox; + }]]); +} + +- (void)testRestoreTransactions { + XCTestExpectation *expectation = + [self expectationWithDescription:@"result successfully restore transactions"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; + SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block BOOL callbackInvoked = NO; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testRetrieveReceiptDataSuccess { + XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[NSString class]]); +} + +- (void)testRetrieveReceiptDataError { + XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + self.receiptManagerStub.returnError = YES; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[FlutterError class]]); + NSDictionary *details = ((FlutterError *)result).details; + XCTAssertNotNil(details[@"error"]); + NSNumber *errorCode = (NSNumber *)details[@"error"][@"code"]; + XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); +} + +- (void)testRefreshReceiptRequest { + XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; + __block BOOL result = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + result = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(result); +} + +- (void)testPresentCodeRedemptionSheet { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; + __block BOOL callbackInvoked = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + callbackInvoked = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testGetPendingTransactions { + XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; + SKPaymentQueue *mockQueue = OCMClassMock(SKPaymentQueue.class); + NSDictionary *transactionMap = @{ + @"transactionIdentifier" : [NSNull null], + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] + initWithMap:transactionMap] ]); + + __block NSArray *resultArray; + 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; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqualObjects(resultArray, @[ transactionMap ]); +} + +- (void)testStartObservingPaymentQueue { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Should return success result"]; + FlutterMethodCall *startCall = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" + arguments:nil]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:startCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler startObservingPaymentQueue]); +} + +- (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 _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]); +} + +- (void)testRegisterPaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + + // Verify the delegate is nil before we register one. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is not nil after we registered one. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + } +} + +- (void)testRemovePaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); + + // Verify the delegate is not nil before removing it. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is nill after removing it. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + } +} + +- (void)testShowPriceConsentIfNeeded { + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" + arguments:nil]; + + FIAPaymentQueueHandler *mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); + self.plugin.paymentQueueHandler = mockQueueHandler; + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (@available(iOS 13.4, *)) { + OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); + } else { + OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); + } +#pragma clang diagnostic pop +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..6c40a6cd0c4a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/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 + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + 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 new file mode 100644 index 000000000000..2f8d5857c8d8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m @@ -0,0 +1,420 @@ +// Copyright 2013 The Flutter Authors. 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 "Stubs.h" + +@import in_app_purchase_storekit; + +@interface PaymentQueueTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; + +@end + +@implementation PaymentQueueTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + self.productMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + @"subscriptionPeriod" : self.periodMap, + @"introductoryPrice" : self.discountMap, + @"subscriptionGroupIdentifier" : @"com.group" + }; + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : [NSNull null]}; +} + +- (void)testTransactionPurchased { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchased transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + 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); + XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); +} + +- (void)testTransactionFailed { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get failed transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + 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); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testTransactionRestored { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get restored transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateRestored; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + 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); + XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); +} + +- (void)testTransactionPurchasing { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchasing transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchasing; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + 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); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testTransactionDeferred { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get deffered transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + 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); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testFinishTransaction { + XCTestExpectation *expectation = + [self expectationWithDescription:@"handler.transactions should be empty."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqual(transactions.count, 1); + SKPaymentTransaction *transaction = transactions[0]; + [handler finishTransaction:transaction]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqual(transactions.count, 1); + [expectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + 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_ios/example/ios/RunnerTests/ProductRequestHandlerTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m similarity index 99% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/ProductRequestHandlerTests.m rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m index 16b9462ce11d..ac36aae5acb5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/ProductRequestHandlerTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m @@ -5,7 +5,7 @@ #import #import "Stubs.h" -@import in_app_purchase_ios; +@import in_app_purchase_storekit; #pragma tests start here diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h similarity index 98% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h index 085a06337386..d4e8df3eba72 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h @@ -5,7 +5,7 @@ #import #import -@import in_app_purchase_ios; +@import in_app_purchase_storekit; NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(11.2), macos(10.13.2)) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m similarity index 96% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m rename to packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m index 364505d6754a..e4277d3edd59 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m @@ -61,6 +61,14 @@ - (instancetype)initWithMap:(NSDictionary *)map { [self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null] forKey:@"subscriptionGroupIdentifier"]; } + if (@available(iOS 12.2, *)) { + NSMutableArray *discounts = [[NSMutableArray alloc] init]; + for (NSDictionary *discountMap in map[@"discounts"]) { + [discounts addObject:[[SKProductDiscountStub alloc] initWithMap:discountMap]]; + } + + [self setValue:discounts forKey:@"discounts"]; + } } 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 new file mode 100644 index 000000000000..c4e1ac1d059d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m @@ -0,0 +1,369 @@ +// Copyright 2013 The Flutter Authors. 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 "Stubs.h" + +@import in_app_purchase_storekit; + +@interface TranslatorTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; +@property(strong, nonatomic) NSDictionary *paymentMap; +@property(copy, nonatomic) NSDictionary *paymentDiscountMap; +@property(strong, nonatomic) NSDictionary *transactionMap; +@property(strong, nonatomic) NSDictionary *errorMap; +@property(strong, nonatomic) NSDictionary *localeMap; +@property(strong, nonatomic) NSDictionary *storefrontMap; +@property(strong, nonatomic) NSDictionary *storefrontAndPaymentTransactionMap; + +@end + +@implementation TranslatorTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + + self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + }]; + if (@available(iOS 11.2, *)) { + self.productMap[@"subscriptionPeriod"] = self.periodMap; + self.productMap[@"introductoryPrice"] = self.discountMap; + } + if (@available(iOS 12.2, *)) { + self.productMap[@"discounts"] = @[ self.discountMap ]; + } + + if (@available(iOS 12.0, *)) { + self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; + } + + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; + self.paymentMap = @{ + @"productIdentifier" : @"123", + @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + @"quantity" : @(2), + @"applicationUsername" : @"app user name", + @"simulatesAskToBuyInSandbox" : @(NO) + }; + self.paymentDiscountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + NSDictionary *originalTransactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + self.transactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : originalTransactionMap, + }; + self.errorMap = @{ + @"code" : @(123), + @"domain" : @"test_domain", + @"userInfo" : @{ + @"key" : @"value", + } + }; + self.storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + + self.storefrontAndPaymentTransactionMap = @{ + @"storefront" : self.storefrontMap, + @"transaction" : self.transactionMap, + }; +} + +- (void)testSKProductSubscriptionPeriodStubToMap { + if (@available(iOS 11.2, *)) { + SKProductSubscriptionPeriodStub *period = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; + XCTAssertEqualObjects(map, self.periodMap); + } +} + +- (void)testSKProductDiscountStubToMap { + if (@available(iOS 11.2, *)) { + SKProductDiscountStub *discount = [[SKProductDiscountStub alloc] initWithMap:self.discountMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMap); + } +} + +- (void)testProductToMap { + SKProductStub *product = [[SKProductStub alloc] initWithMap:self.productMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProduct:product]; + XCTAssertEqualObjects(map, self.productMap); +} + +- (void)testProductResponseToMap { + SKProductsResponseStub *response = + [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; + XCTAssertEqualObjects(map, self.productResponseMap); +} + +- (void)testPaymentToMap { + SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:self.paymentMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPayment:payment]; + XCTAssertEqualObjects(map, self.paymentMap); +} + +- (void)testPaymentTransactionToMap { + // payment is not KVC, cannot test payment field. + SKPaymentTransactionStub *paymentTransaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; + XCTAssertEqualObjects(map, self.transactionMap); +} + +- (void)testError { + NSErrorStub *error = [[NSErrorStub alloc] initWithMap:self.errorMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + 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"]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; + XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); + XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); + } +} + +- (void)testSKStorefrontToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront]; + XCTAssertEqualObjects(map, self.storefrontMap); + } +} + +- (void)testSKStorefrontAndSKPaymentTransactionToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + SKPaymentTransaction *transaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront + andSKPaymentTransaction:transaction]; + XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); + } +} + +- (void)testSKPaymentDiscountFromMap { + if (@available(iOS 12.2, *)) { + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:self.paymentDiscountMap withError:&error]; + + XCTAssertEqual(paymentDiscount.identifier, self.paymentDiscountMap[@"identifier"]); + XCTAssertEqual(paymentDiscount.keyIdentifier, self.paymentDiscountMap[@"keyIdentifier"]); + XCTAssertEqualObjects(paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:self.paymentDiscountMap[@"nonce"]]); + XCTAssertEqual(paymentDiscount.signature, self.paymentDiscountMap[@"signature"]); + XCTAssertEqual(paymentDiscount.timestamp, self.paymentDiscountMap[@"timestamp"]); + } +} + +- (void)testSKPaymentDiscountFromMapMissingIdentifier { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : value, + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'identifier' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingKeyIdentifier { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : value, + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'keyIdentifier' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingNonce { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : value, + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects(error, + @"When specifying a payment discount the 'nonce' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingSignature { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : value, + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'signature' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingTimestamp { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @"", @(-1) ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : value, + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'timestamp' field is mandatory."); + } + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/consumable_store.dart similarity index 75% rename from packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart rename to packages/in_app_purchase/in_app_purchase_storekit/example/lib/consumable_store.dart index 4d10a50e1ee8..f8791d3b18c0 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/consumable_store.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'package:shared_preferences/shared_preferences.dart'; +// ignore: avoid_classes_with_only_static_members /// A store of consumable items. /// -/// This is a development prototype tha stores consumables in the shared +/// This is a development prototype that stores consumables in the shared /// preferences. Do not use this in real world apps. class ConsumableStore { static const String _kPrefKey = 'consumables'; - static Future _writes = Future.value(); + static Future _writes = Future.value(); /// Adds a consumable with ID `id` to the store. /// @@ -32,19 +33,19 @@ class ConsumableStore { /// Returns the list of consumables from the store. static Future> load() async { return (await SharedPreferences.getInstance()).getStringList(_kPrefKey) ?? - []; + []; } static Future _doSave(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); + final List cached = await load(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); cached.add(id); await prefs.setStringList(_kPrefKey, cached); } static Future _doConsume(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); + final List cached = await load(); + final SharedPreferences prefs = await SharedPreferences.getInstance(); cached.remove(id); await prefs.setStringList(_kPrefKey, cached); } diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/example_payment_queue_delegate.dart similarity index 91% rename from packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart rename to packages/in_app_purchase/in_app_purchase_storekit/example/lib/example_payment_queue_delegate.dart index dfebdf9cdf98..ba04ab12f37d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/example_payment_queue_delegate.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_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; /// Example implementation of the /// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). 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 new file mode 100644 index 000000000000..5849b17d59d6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart @@ -0,0 +1,430 @@ +// Copyright 2013 The Flutter Authors. 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 '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'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + // When using the Android plugin directly it is mandatory to register + // the plugin as default instance as part of initializing the app. + InAppPurchaseStoreKitPlatform.registerPlatform(); + + runApp(_MyApp()); +} + +const bool _kAutoConsume = true; + +const String _kConsumableId = 'consumable'; +const String _kUpgradeId = 'upgrade'; +const String _kSilverSubscriptionId = 'subscription_silver'; +const String _kGoldSubscriptionId = 'subscription_gold'; +const List _kProductIds = [ + _kConsumableId, + _kUpgradeId, + _kSilverSubscriptionId, + _kGoldSubscriptionId, +]; + +class _MyApp extends StatefulWidget { + @override + State<_MyApp> createState() => _MyAppState(); +} + +class _MyAppState extends State<_MyApp> { + final InAppPurchaseStoreKitPlatform _iapStoreKitPlatform = + InAppPurchasePlatform.instance as InAppPurchaseStoreKitPlatform; + final InAppPurchaseStoreKitPlatformAddition _iapStoreKitPlatformAddition = + InAppPurchasePlatformAddition.instance! + as InAppPurchaseStoreKitPlatformAddition; + late StreamSubscription> _subscription; + List _notFoundIds = []; + List _products = []; + List _purchases = []; + List _consumables = []; + bool _isAvailable = false; + bool _purchasePending = false; + bool _loading = true; + String? _queryProductError; + + @override + void initState() { + final Stream> purchaseUpdated = + _iapStoreKitPlatform.purchaseStream; + _subscription = + purchaseUpdated.listen((List purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (Object error) { + // handle error here. + }); + + // Register the example payment queue delegate + _iapStoreKitPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + + initStoreInfo(); + super.initState(); + } + + Future initStoreInfo() async { + final bool isAvailable = await _iapStoreKitPlatform.isAvailable(); + if (!isAvailable) { + setState(() { + _isAvailable = isAvailable; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + final ProductDetailsResponse productDetailResponse = + await _iapStoreKitPlatform.queryProductDetails(_kProductIds.toSet()); + if (productDetailResponse.error != null) { + setState(() { + _queryProductError = productDetailResponse.error!.message; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + if (productDetailResponse.productDetails.isEmpty) { + setState(() { + _queryProductError = null; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + final List consumables = await ConsumableStore.load(); + setState(() { + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = consumables; + _purchasePending = false; + _loading = false; + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List stack = []; + if (_queryProductError == null) { + stack.add( + ListView( + children: [ + _buildConnectionCheckTile(), + _buildProductList(), + _buildConsumableBox(), + _buildRestoreButton(), + ], + ), + ); + } else { + stack.add(Center( + child: Text(_queryProductError!), + )); + } + if (_purchasePending) { + stack.add( + Stack( + children: const [ + Opacity( + opacity: 0.3, + child: ModalBarrier(dismissible: false, color: Colors.grey), + ), + Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } + + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('IAP Example'), + ), + body: Stack( + children: stack, + ), + ), + ); + } + + Card _buildConnectionCheckTile() { + if (_loading) { + return const Card(child: ListTile(title: Text('Trying to connect...'))); + } + 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'}.'), + ); + final List children = [storeHeader]; + + if (!_isAvailable) { + children.addAll([ + const Divider(), + ListTile( + title: Text('Not connected', + style: TextStyle(color: ThemeData.light().errorColor)), + subtitle: const Text( + 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), + ), + ]); + } + return Card(child: Column(children: children)); + } + + Card _buildProductList() { + if (_loading) { + return const Card( + child: ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching products...'))); + } + if (!_isAvailable) { + return const Card(); + } + const ListTile productHeader = ListTile(title: Text('Products for Sale')); + final List productList = []; + if (_notFoundIds.isNotEmpty) { + productList.add(ListTile( + title: Text('[${_notFoundIds.join(", ")}] not found', + style: TextStyle(color: ThemeData.light().errorColor)), + subtitle: const Text( + 'This app needs special configuration to run. Please see example/README.md for instructions.'))); + } + + // This loading previous purchases code is just a demo. Please do not use this as it is. + // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it. + // We recommend that you use your own server to verify the purchase data. + final Map purchases = + Map.fromEntries( + _purchases.map((PurchaseDetails purchase) { + if (purchase.pendingCompletePurchase) { + _iapStoreKitPlatform.completePurchase(purchase); + } + return MapEntry(purchase.productID, purchase); + })); + productList.addAll(_products.map( + (ProductDetails productDetails) { + final PurchaseDetails? previousPurchase = purchases[productDetails.id]; + return ListTile( + title: Text( + productDetails.title, + ), + subtitle: Text( + productDetails.description, + ), + trailing: previousPurchase != null + ? IconButton( + onPressed: () { + _iapStoreKitPlatformAddition.showPriceConsentIfNeeded(); + }, + 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: () { + final PurchaseParam purchaseParam = PurchaseParam( + productDetails: productDetails, + applicationUserName: null, + ); + if (productDetails.id == _kConsumableId) { + _iapStoreKitPlatform.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume || Platform.isIOS); + } else { + _iapStoreKitPlatform.buyNonConsumable( + purchaseParam: purchaseParam); + } + }, + child: Text(productDetails.price), + )); + }, + )); + + return Card( + child: Column( + children: [productHeader, const Divider()] + productList)); + } + + Card _buildConsumableBox() { + if (_loading) { + return const Card( + child: ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching consumables...'))); + } + if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { + return const Card(); + } + const ListTile consumableHeader = + ListTile(title: Text('Purchased consumables')); + final List tokens = _consumables.map((String id) { + return GridTile( + child: IconButton( + icon: const Icon( + Icons.stars, + size: 42.0, + color: Colors.orange, + ), + splashColor: Colors.yellowAccent, + onPressed: () => consume(id), + ), + ); + }).toList(); + return Card( + child: Column(children: [ + consumableHeader, + const Divider(), + GridView.count( + crossAxisCount: 5, + shrinkWrap: true, + padding: const EdgeInsets.all(16.0), + children: tokens, + ) + ])); + } + + Widget _buildRestoreButton() { + if (_loading) { + return Container(); + } + + return Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + 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: () => _iapStoreKitPlatform.restorePurchases(), + child: const Text('Restore purchases'), + ), + ], + ), + ); + } + + Future consume(String id) async { + await ConsumableStore.consume(id); + final List consumables = await ConsumableStore.load(); + setState(() { + _consumables = consumables; + }); + } + + void showPendingUI() { + setState(() { + _purchasePending = true; + }); + } + + Future deliverProduct(PurchaseDetails purchaseDetails) async { + // IMPORTANT!! Always verify purchase details before delivering the product. + if (purchaseDetails.productID == _kConsumableId) { + await ConsumableStore.save(purchaseDetails.purchaseID!); + final List consumables = await ConsumableStore.load(); + setState(() { + _purchasePending = false; + _consumables = consumables; + }); + } else { + setState(() { + _purchases.add(purchaseDetails); + _purchasePending = false; + }); + } + } + + void handleError(IAPError error) { + setState(() { + _purchasePending = false; + }); + } + + Future _verifyPurchase(PurchaseDetails purchaseDetails) { + // IMPORTANT!! Always verify a purchase before delivering the product. + // For the purpose of an example, we directly return true. + return Future.value(true); + } + + void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { + // handle invalid purchase here if _verifyPurchase` failed. + } + + void _listenToPurchaseUpdated(List purchaseDetailsList) { + purchaseDetailsList.forEach(_handleReportedPurchaseState); + } + + Future _handleReportedPurchaseState( + PurchaseDetails purchaseDetails) async { + if (purchaseDetails.status == PurchaseStatus.pending) { + showPendingUI(); + } else { + if (purchaseDetails.status == PurchaseStatus.error) { + handleError(purchaseDetails.error!); + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { + final bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + await deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + return; + } + } + + if (purchaseDetails.pendingCompletePurchase) { + await _iapStoreKitPlatform.completePurchase(purchaseDetails); + } + } + } +} 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 new file mode 100644 index 000000000000..a98e1693aa40 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: in_app_purchase_storekit_example +description: Demonstrates how to use the in_app_purchase_storekit plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.0.0 + in_app_purchase_storekit: + # When depending on this package from a real application you should use: + # in_app_purchase_storekit: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + shared_preferences: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/test/integration_test.dart similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test.dart rename to packages/in_app_purchase/in_app_purchase_storekit/example/test_driver/test/integration_test.dart diff --git a/packages/connectivity/connectivity/ios/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep similarity index 100% rename from packages/connectivity/connectivity/ios/Assets/.gitkeep rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h similarity index 80% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h index 95a5edc245dc..eb97ceb44754 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h @@ -20,6 +20,10 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount API_AVAILABLE(ios(11.2)); +// Converts an array of SKProductDiscount instances into an array of dictionaries. ++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: + (nonnull NSArray *)productDiscounts API_AVAILABLE(ios(12.2)); + // Converts an instance of SKProductsResponse into a dictionary. + (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; @@ -47,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN andSKPaymentTransaction:(SKPaymentTransaction *)transaction API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); +// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary. ++ (nullable SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map + withError:(NSString *_Nullable *_Nullable)error + API_AVAILABLE(ios(12.2)); + @end ; 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 new file mode 100644 index 000000000000..5d87a68de67c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m @@ -0,0 +1,293 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FIAObjectTranslator.h" + +#pragma mark - SKProduct Coders + +@implementation FIAObjectTranslator + ++ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { + if (!product) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"localizedDescription" : product.localizedDescription ?: [NSNull null], + @"localizedTitle" : product.localizedTitle ?: [NSNull null], + @"productIdentifier" : product.productIdentifier ?: [NSNull null], + @"price" : product.price.description ?: [NSNull null] + + }]; + // 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. + // https://github.com/flutter/flutter/issues/26610 + [map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null] + forKey:@"priceLocale"]; + if (@available(iOS 11.2, *)) { + [map setObject:[FIAObjectTranslator + getMapFromSKProductSubscriptionPeriod:product.subscriptionPeriod] + ?: [NSNull null] + forKey:@"subscriptionPeriod"]; + } + if (@available(iOS 11.2, *)) { + [map setObject:[FIAObjectTranslator getMapFromSKProductDiscount:product.introductoryPrice] + ?: [NSNull null] + forKey:@"introductoryPrice"]; + } + if (@available(iOS 12.2, *)) { + [map setObject:[FIAObjectTranslator getMapArrayFromSKProductDiscounts:product.discounts] + forKey:@"discounts"]; + } + if (@available(iOS 12.0, *)) { + [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null] + forKey:@"subscriptionGroupIdentifier"]; + } + return map; +} + ++ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period { + if (!period) { + return nil; + } + return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; +} + ++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: + (nonnull NSArray *)productDiscounts { + NSMutableArray *discountsMapArray = [[NSMutableArray alloc] init]; + + for (SKProductDiscount *productDiscount in productDiscounts) { + [discountsMapArray addObject:[FIAObjectTranslator getMapFromSKProductDiscount:productDiscount]]; + } + + return discountsMapArray; +} + ++ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { + if (!discount) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : discount.price.description ?: [NSNull null], + @"numberOfPeriods" : @(discount.numberOfPeriods), + @"subscriptionPeriod" : + [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] + ?: [NSNull null], + @"paymentMode" : @(discount.paymentMode) + }]; + + // 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. + // https://github.com/flutter/flutter/issues/26610 + [map setObject:[FIAObjectTranslator getMapFromNSLocale:discount.priceLocale] ?: [NSNull null] + forKey:@"priceLocale"]; + return map; +} + ++ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse { + if (!productResponse) { + return nil; + } + NSMutableArray *productsMapArray = [NSMutableArray new]; + for (SKProduct *product in productResponse.products) { + [productsMapArray addObject:[FIAObjectTranslator getMapFromSKProduct:product]]; + } + return @{ + @"products" : productsMapArray, + @"invalidProductIdentifiers" : productResponse.invalidProductIdentifiers ?: @[] + }; +} + ++ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { + if (!payment) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"productIdentifier" : payment.productIdentifier ?: [NSNull null], + @"requestData" : payment.requestData ? [[NSString alloc] initWithData:payment.requestData + encoding:NSUTF8StringEncoding] + : [NSNull null], + @"quantity" : @(payment.quantity), + @"applicationUsername" : payment.applicationUsername ?: [NSNull null] + }]; + [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; + return map; +} + ++ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale { + if (!locale) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; + [map setObject:[locale objectForKey:NSLocaleCurrencySymbol] ?: [NSNull null] + forKey:@"currencySymbol"]; + [map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null] + forKey:@"currencyCode"]; + [map setObject:[locale objectForKey:NSLocaleCountryCode] ?: [NSNull null] forKey:@"countryCode"]; + return map; +} + ++ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { + if (!map) { + return nil; + } + SKMutablePayment *payment = [[SKMutablePayment alloc] init]; + payment.productIdentifier = map[@"productIdentifier"]; + NSString *utf8String = map[@"requestData"]; + payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; + payment.quantity = [map[@"quantity"] integerValue]; + payment.applicationUsername = map[@"applicationUsername"]; + payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; + return payment; +} + ++ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if (!transaction) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"error" : [FIAObjectTranslator getMapFromNSError:transaction.error] ?: [NSNull null], + @"payment" : transaction.payment ? [FIAObjectTranslator getMapFromSKPayment:transaction.payment] + : [NSNull null], + @"originalTransaction" : transaction.originalTransaction + ? [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction.originalTransaction] + : [NSNull null], + @"transactionTimeStamp" : transaction.transactionDate + ? @(transaction.transactionDate.timeIntervalSince1970) + : [NSNull null], + @"transactionIdentifier" : transaction.transactionIdentifier ?: [NSNull null], + @"transactionState" : @(transaction.transactionState) + }]; + + return map; +} + ++ (NSDictionary *)getMapFromNSError:(NSError *)error { + if (!error) { + return nil; + } + + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + for (NSErrorUserInfoKey key in error.userInfo) { + id value = error.userInfo[key]; + 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; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"countryCode" : storefront.countryCode, + @"identifier" : storefront.identifier + }]; + + return map; +} + ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if (!storefront || !transaction) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], + @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] + }]; + + return map; +} + ++ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map + withError:(NSString **)error { + if (!map || map.count <= 0) { + return nil; + } + + NSString *identifier = map[@"identifier"]; + NSString *keyIdentifier = map[@"keyIdentifier"]; + NSString *nonce = map[@"nonce"]; + NSString *signature = map[@"signature"]; + NSNumber *timestamp = map[@"timestamp"]; + + if (!identifier || ![identifier isKindOfClass:NSString.class] || + [identifier isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'identifier' field is mandatory."; + } + return nil; + } + + if (!keyIdentifier || ![keyIdentifier isKindOfClass:NSString.class] || + [keyIdentifier isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'keyIdentifier' field is mandatory."; + } + return nil; + } + + if (!nonce || ![nonce isKindOfClass:NSString.class] || [nonce isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'nonce' field is mandatory."; + } + return nil; + } + + if (!signature || ![signature isKindOfClass:NSString.class] || [signature isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'signature' field is mandatory."; + } + return nil; + } + + if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) { + if (error) { + *error = @"When specifying a payment discount the 'timestamp' field is mandatory."; + } + return nil; + } + + SKPaymentDiscount *discount = + [[SKPaymentDiscount alloc] initWithIdentifier:identifier + keyIdentifier:keyIdentifier + nonce:[[NSUUID alloc] initWithUUIDString:nonce] + signature:signature + timestamp:timestamp]; + + return discount; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m 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 new file mode 100644 index 000000000000..bb074aa6c577 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "FIATransactionCache.h" + +@class SKPaymentTransaction; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^TransactionsUpdated)(NSArray *transactions); +typedef void (^TransactionsRemoved)(NSArray *transactions); +typedef void (^RestoreTransactionFailed)(NSError *error); +typedef void (^RestoreCompletedTransactionsFinished)(void); +typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); +typedef void (^UpdatedDownloads)(NSArray *downloads); + +@interface FIAPaymentQueueHandler : NSObject + +@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 + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + 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; +- (void)presentCodeRedemptionSheet; +- (NSArray *)getUnfinishedTransactions; + +// This method needs to be called before any other methods. +- (void)startObservingPaymentQueue; +// Call this method when the Flutter app is no longer listening +- (void)stopObservingPaymentQueue; + +// Appends a payment to the SKPaymentQueue. +// +// @param payment Payment object to be added to the payment queue. +// @return whether "addPayment" was successful. +- (BOOL)addPayment:(SKPayment *)payment; + +// Displays the price consent sheet. +// +// The price consent sheet is only displayed when the following +// it true: +// - You have increased the price of the subscription in App Store Connect. +// - The subscriber has not yet responded to a price consent query. +// Otherwise the method has no effect. +- (void)showPriceConsentIfNeeded API_AVAILABLE(ios(13.4)); + +@end + +NS_ASSUME_NONNULL_END 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 new file mode 100644 index 000000000000..59fdceded2bc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m @@ -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. + +#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 + +- (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 { + 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; + _transactionsUpdated = transactionsUpdated; + _transactionsRemoved = transactionsRemoved; + _restoreTransactionFailed = restoreTransactionFailed; + _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; + _shouldAddStorePayment = shouldAddStorePayment; + _updatedDownloads = updatedDownloads; + _transactionCache = transactionCache; + + [_queue addTransactionObserver:self]; + if (@available(iOS 13.0, macOS 10.15, *)) { + queue.delegate = self.delegate; + } + } + return self; +} + +- (void)startObservingPaymentQueue { + self.observingTransactions = YES; + + [self processCachedTransactions]; +} + +- (void)stopObservingPaymentQueue { + // 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 { + for (SKPaymentTransaction *transaction in self.queue.transactions) { + if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { + return NO; + } + } + [self.queue addPayment:payment]; + return YES; +} + +- (void)finishTransaction:(SKPaymentTransaction *)transaction { + [self.queue finishTransaction:transaction]; +} + +- (void)restoreTransactions:(nullable NSString *)applicationName { + if (applicationName) { + [self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName]; + } else { + [self.queue restoreCompletedTransactions]; + } +} + +- (void)presentCodeRedemptionSheet { + if (@available(iOS 14, *)) { + [self.queue presentCodeRedemptionSheet]; + } else { + NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); + } +} + +- (void)showPriceConsentIfNeeded { + [self.queue showPriceConsentIfNeeded]; +} + +#pragma mark - observing + +// Sent when the transaction array has changed (additions or state changes). Client should check +// 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); +} + +// 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); +} + +// Sent when an error is encountered while adding transactions from the user's purchase history back +// to the queue. +- (void)paymentQueue:(SKPaymentQueue *)queue + restoreCompletedTransactionsFailedWithError:(NSError *)error { + self.restoreTransactionFailed(error); +} + +// Sent when all transactions from the user's purchase history have successfully been added back to +// the queue. +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { + self.paymentQueueRestoreCompletedTransactionsFinished(); +} + +// 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); +} + +// Sent when a user initiates an IAP buy from the App Store +- (BOOL)paymentQueue:(SKPaymentQueue *)queue + shouldAddStorePayment:(SKPayment *)payment + forProduct:(SKProduct *)product { + return (self.shouldAddStorePayment(payment, product)); +} + +- (NSArray *)getUnfinishedTransactions { + return self.queue.transactions; +} + +@end 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_ios/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m similarity index 95% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m rename to packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m index 7e2d2ca80675..d64c24563b62 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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" @@ -203,6 +199,25 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { ? NO : [simulatesAskToBuyInSandbox boolValue]; + if (@available(iOS 12.2, *)) { + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator + getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"] + withError:&error]; + + if (error) { + result([FlutterError + errorWithCode:@"storekit_invalid_payment_discount_object" + message:[NSString stringWithFormat:@"You have requested a payment and specified a " + @"payment discount with invalid properties. %@", + error] + details:call.arguments]); + return; + } + + payment.paymentDiscount = paymentDiscount; + } + if (![self.paymentQueueHandler addPayment:payment]) { result([FlutterError errorWithCode:@"storekit_duplicate_product_object" @@ -327,7 +342,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]; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec new file mode 100644 index 000000000000..dd83234ac4ad --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec @@ -0,0 +1,24 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'in_app_purchase_storekit' + s.version = '0.0.1' + s.summary = 'Flutter In App Purchase iOS' + s.description = <<-DESC +A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit' } + # TODO(mvanbeusekom): update URL when in_app_purchase_storekit package is published. + # Updating it before the package is published will cause a lint error and block the tree. + s.documentation_url = 'https://pub.dev/packages/in_app_purchase' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/in_app_purchase_storekit.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/in_app_purchase_storekit.dart new file mode 100644 index 000000000000..7e8bf6ccceed --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/in_app_purchase_storekit.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 'src/in_app_purchase_storekit_platform.dart'; +export 'src/in_app_purchase_storekit_platform_addition.dart'; +export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/channel.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/channel.dart 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 new file mode 100644 index 000000000000..f81b36699834 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -0,0 +1,254 @@ +// Copyright 2013 The Flutter Authors. 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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../in_app_purchase_storekit.dart'; +import '../store_kit_wrappers.dart'; + +/// [IAPError.code] code for failed purchases. +const String kPurchaseErrorCode = 'purchase_error'; + +/// Indicates store front is Apple AppStore. +const String kIAPSource = 'app_store'; + +/// An [InAppPurchasePlatform] that wraps StoreKit. +/// +/// This translates various `StoreKit` calls and responses into the +/// generic plugin API. +class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { + /// Creates an [InAppPurchaseStoreKitPlatform] object. + /// + /// This constructor should only be used for testing, for any other purpose + /// get the connection from the [instance] getter. + @visibleForTesting + InAppPurchaseStoreKitPlatform(); + + static late SKPaymentQueueWrapper _skPaymentQueueWrapper; + static late _TransactionObserver _observer; + + @override + Stream> get purchaseStream => + _observer.purchaseUpdatedController.stream; + + /// Callback handler for transaction status changes. + @visibleForTesting + static SKTransactionObserverWrapper get observer => _observer; + + /// Registers this class as the default instance of [InAppPurchasePlatform]. + static void registerPlatform() { + // Register the [InAppPurchaseStoreKitPlatformAddition] containing + // StoreKit-specific functionality. + InAppPurchasePlatformAddition.instance = + InAppPurchaseStoreKitPlatformAddition(); + + // Register the platform-specific implementation of the idiomatic + // InAppPurchase API. + InAppPurchasePlatform.instance = InAppPurchaseStoreKitPlatform(); + + _skPaymentQueueWrapper = SKPaymentQueueWrapper(); + + // Create a purchaseUpdatedController and notify the native side when to + // start of stop sending updates. + final StreamController> updateController = + StreamController>.broadcast( + onListen: () => _skPaymentQueueWrapper.startObservingTransactionQueue(), + onCancel: () => _skPaymentQueueWrapper.stopObservingTransactionQueue(), + ); + _observer = _TransactionObserver(updateController); + _skPaymentQueueWrapper.setTransactionObserver(observer); + } + + @override + Future isAvailable() => SKPaymentQueueWrapper.canMakePayments(); + + @override + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { + await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( + productIdentifier: purchaseParam.productDetails.id, + quantity: + purchaseParam is AppStorePurchaseParam ? purchaseParam.quantity : 1, + applicationUsername: purchaseParam.applicationUserName, + simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam && + purchaseParam.simulatesAskToBuyInSandbox, + requestData: null)); + + return true; // There's no error feedback from iOS here to return. + } + + @override + Future buyConsumable( + {required PurchaseParam purchaseParam, bool autoConsume = true}) { + assert(autoConsume == true, 'On iOS, we should always auto consume'); + return buyNonConsumable(purchaseParam: purchaseParam); + } + + @override + Future completePurchase(PurchaseDetails purchase) { + assert( + purchase is AppStorePurchaseDetails, + 'On iOS, the `purchase` should always be of type `AppStorePurchaseDetails`.', + ); + + return _skPaymentQueueWrapper.finishTransaction( + (purchase as AppStorePurchaseDetails).skPaymentTransaction, + ); + } + + @override + Future restorePurchases({String? applicationUserName}) async { + return _observer + .restoreTransactions( + queue: _skPaymentQueueWrapper, + applicationUserName: applicationUserName) + .whenComplete(() => _observer.cleanUpRestoredTransactions()); + } + + /// Query the product detail list. + /// + /// This method only returns [ProductDetailsResponse]. + /// To get detailed Store Kit product list, use [SkProductResponseWrapper.startProductRequest] + /// to get the [SKProductResponseWrapper]. + @override + Future queryProductDetails( + Set identifiers) async { + final SKRequestMaker requestMaker = SKRequestMaker(); + SkProductResponseWrapper response; + PlatformException? exception; + try { + response = await requestMaker.startProductRequest(identifiers.toList()); + } on PlatformException catch (e) { + exception = e; + response = SkProductResponseWrapper( + products: const [], + invalidProductIdentifiers: identifiers.toList()); + } + List productDetails = []; + if (response.products != null) { + productDetails = response.products + .map((SKProductWrapper productWrapper) => + AppStoreProductDetails.fromSKProduct(productWrapper)) + .toList(); + } + List invalidIdentifiers = response.invalidProductIdentifiers; + if (productDetails.isEmpty) { + invalidIdentifiers = identifiers.toList(); + } + final ProductDetailsResponse productDetailsResponse = + ProductDetailsResponse( + productDetails: productDetails, + notFoundIDs: invalidIdentifiers, + error: exception == null + ? null + : IAPError( + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details), + ); + return productDetailsResponse; + } +} + +enum _TransactionRestoreState { + notRunning, + waitingForTransactions, + receivedTransaction, +} + +class _TransactionObserver implements SKTransactionObserverWrapper { + _TransactionObserver(this.purchaseUpdatedController); + + final StreamController> purchaseUpdatedController; + + Completer? _restoreCompleter; + late String _receiptData; + _TransactionRestoreState _transactionRestoreState = + _TransactionRestoreState.notRunning; + + Future restoreTransactions({ + required SKPaymentQueueWrapper queue, + String? applicationUserName, + }) { + _transactionRestoreState = _TransactionRestoreState.waitingForTransactions; + _restoreCompleter = Completer(); + queue.restoreTransactions(applicationUserName: applicationUserName); + return _restoreCompleter!.future; + } + + void cleanUpRestoredTransactions() { + _restoreCompleter = null; + } + + @override + void updatedTransactions( + {required List transactions}) { + _handleTransationUpdates(transactions); + } + + @override + void removedTransactions( + {required List transactions}) {} + + /// Triggered when there is an error while restoring transactions. + @override + void restoreCompletedTransactionsFailed({required SKError error}) { + _restoreCompleter!.completeError(error); + _transactionRestoreState = _TransactionRestoreState.notRunning; + } + + @override + void paymentQueueRestoreCompletedTransactionsFinished() { + _restoreCompleter!.complete(); + + // If no restored transactions were received during the restore session + // emit an empty list of purchase details to inform listeners that the + // restore session finished without any results. + if (_transactionRestoreState == + _TransactionRestoreState.waitingForTransactions) { + purchaseUpdatedController.add([]); + } + + _transactionRestoreState = _TransactionRestoreState.notRunning; + } + + @override + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + // In this unified API, we always return true to keep it consistent with the behavior on Google Play. + return true; + } + + Future getReceiptData() async { + try { + _receiptData = await SKReceiptManager.retrieveReceiptData(); + } catch (e) { + _receiptData = ''; + } + return _receiptData; + } + + Future _handleTransationUpdates( + List transactions) async { + if (_transactionRestoreState == + _TransactionRestoreState.waitingForTransactions && + transactions.any((SKPaymentTransactionWrapper transaction) => + transaction.transactionState == + SKPaymentTransactionStateWrapper.restored)) { + _transactionRestoreState = _TransactionRestoreState.receivedTransaction; + } + + final String receiptData = await getReceiptData(); + final List purchases = transactions + .map((SKPaymentTransactionWrapper transaction) => + AppStorePurchaseDetails.fromSKTransaction(transaction, receiptData)) + .toList(); + + purchaseUpdatedController.add(purchases); + } +} 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 new file mode 100644 index 000000000000..87655df53d34 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; + +import '../store_kit_wrappers.dart'; + +/// Contains InApp Purchase features that are only available on iOS. +class InAppPurchaseStoreKitPlatformAddition + extends InAppPurchasePlatformAddition { + /// Present Code Redemption Sheet. + /// + /// Available on devices running iOS 14 and iPadOS 14 and later. + Future presentCodeRedemptionSheet() { + return SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + } + + /// Retry loading purchase data after an initial failure. + /// + /// If no results, a `null` value is returned. + Future refreshPurchaseVerificationData() async { + await SKRequestMaker().startRefreshReceiptRequest(); + try { + final String receipt = await SKReceiptManager.retrieveReceiptData(); + return PurchaseVerificationData( + localVerificationData: receipt, + serverVerificationData: receipt, + source: kIAPSource); + } catch (e) { + print( + 'Something is wrong while fetching the receipt, this normally happens when the app is ' + 'running on a simulator: $e'); + return null; + } + } + + /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. + /// + /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to + /// finish transactions when the storefront changes or if the price consent + /// sheet should be displayed when the price of a subscription has changed. If + /// no delegate is registered iOS will fallback to it's default configuration. + /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). + /// + /// When set to `null` the payment queue delegate will be removed and the + /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). + Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) => + SKPaymentQueueWrapper().setDelegate(delegate); + + /// Shows the price consent sheet if the user has not yet responded to a + /// subscription price change. + /// + /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] + /// (using the [setDelegate] method) and returned `false` when the + /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. + /// + /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). + Future showPriceConsentIfNeeded() => + SKPaymentQueueWrapper().showPriceConsentIfNeeded(); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/README.md similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/README.md 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 new file mode 100644 index 000000000000..1c2bee5a069a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../store_kit_wrappers.dart'; + +part 'enum_converters.g.dart'; + +/// Serializer for [SKPaymentTransactionStateWrapper]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKTransactionStatusConverter()`. +class SKTransactionStatusConverter + implements JsonConverter { + /// Default const constructor. + const SKTransactionStatusConverter(); + + @override + SKPaymentTransactionStateWrapper fromJson(int? json) { + if (json == null) { + return SKPaymentTransactionStateWrapper.unspecified; + } + return $enumDecode( + _$SKPaymentTransactionStateWrapperEnumMap + .cast(), + json); + } + + /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus]. + PurchaseStatus toPurchaseStatus( + SKPaymentTransactionStateWrapper object, SKError? error) { + switch (object) { + case SKPaymentTransactionStateWrapper.purchasing: + case SKPaymentTransactionStateWrapper.deferred: + return PurchaseStatus.pending; + case SKPaymentTransactionStateWrapper.purchased: + return PurchaseStatus.purchased; + case SKPaymentTransactionStateWrapper.restored: + return PurchaseStatus.restored; + case SKPaymentTransactionStateWrapper.failed: + // According to the Apple documentation the error code "2" indicates + // the user cancelled the payment (SKErrorPaymentCancelled) and error + // code "15" indicates the cancellation of the overlay (SKErrorOverlayCancelled). + // An overview of all error codes can be found at: https://developer.apple.com/documentation/storekit/skerrorcode?language=objc + if (error != null && (error.code == 2 || error.code == 15)) { + return PurchaseStatus.canceled; + } + return PurchaseStatus.error; + case SKPaymentTransactionStateWrapper.unspecified: + return PurchaseStatus.error; + } + } + + @override + int toJson(SKPaymentTransactionStateWrapper object) => + _$SKPaymentTransactionStateWrapperEnumMap[object]!; +} + +/// Serializer for [SKSubscriptionPeriodUnit]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKSubscriptionPeriodUnitConverter()`. +class SKSubscriptionPeriodUnitConverter + implements JsonConverter { + /// Default const constructor. + const SKSubscriptionPeriodUnitConverter(); + + @override + SKSubscriptionPeriodUnit fromJson(int? json) { + if (json == null) { + return SKSubscriptionPeriodUnit.day; + } + return $enumDecode( + _$SKSubscriptionPeriodUnitEnumMap + .cast(), + json); + } + + @override + int toJson(SKSubscriptionPeriodUnit object) => + _$SKSubscriptionPeriodUnitEnumMap[object]!; +} + +/// Serializer for [SKProductDiscountPaymentMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKProductDiscountPaymentModeConverter()`. +class SKProductDiscountPaymentModeConverter + implements JsonConverter { + /// Default const constructor. + const SKProductDiscountPaymentModeConverter(); + + @override + SKProductDiscountPaymentMode fromJson(int? json) { + if (json == null) { + return SKProductDiscountPaymentMode.payAsYouGo; + } + return $enumDecode( + _$SKProductDiscountPaymentModeEnumMap + .cast(), + json); + } + + @override + int toJson(SKProductDiscountPaymentMode object) => + _$SKProductDiscountPaymentModeEnumMap[object]!; +} + +// Define a class so we generate serializer helper methods for the enums +// See https://github.com/google/json_serializable.dart/issues/778 +@JsonSerializable() +class _SerializedEnums { + late SKPaymentTransactionStateWrapper response; + late SKSubscriptionPeriodUnit unit; + late SKProductDiscountPaymentMode discountPaymentMode; +} 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 new file mode 100644 index 000000000000..0d05720dc7ae --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enum_converters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() + ..response = + $enumDecode(_$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = $enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = $enumDecode( + _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); + +const _$SKPaymentTransactionStateWrapperEnumMap = { + SKPaymentTransactionStateWrapper.purchasing: 0, + SKPaymentTransactionStateWrapper.purchased: 1, + SKPaymentTransactionStateWrapper.failed: 2, + SKPaymentTransactionStateWrapper.restored: 3, + SKPaymentTransactionStateWrapper.deferred: 4, + SKPaymentTransactionStateWrapper.unspecified: -1, +}; + +const _$SKSubscriptionPeriodUnitEnumMap = { + SKSubscriptionPeriodUnit.day: 0, + SKSubscriptionPeriodUnit.week: 1, + SKSubscriptionPeriodUnit.month: 2, + SKSubscriptionPeriodUnit.year: 3, +}; + +const _$SKProductDiscountPaymentModeEnumMap = { + SKProductDiscountPaymentMode.payAsYouGo: 0, + SKProductDiscountPaymentMode.payUpFront: 1, + SKProductDiscountPaymentMode.freeTrail: 2, + SKProductDiscountPaymentMode.unspecified: -1, +}; diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 96% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart index 2759a296389b..eb88953096e6 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/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 new file mode 100644 index 000000000000..70db7da2e275 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -0,0 +1,585 @@ +// Copyright 2013 The Flutter Authors. 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: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 '../channel.dart'; +import '../in_app_purchase_storekit_platform.dart'; + +part 'sk_payment_queue_wrapper.g.dart'; + +/// A wrapper around +/// [`SKPaymentQueue`](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc). +/// +/// The payment queue contains payment related operations. It communicates with +/// the App Store and presents a user interface for the user to process and +/// authorize payments. +/// +/// Full information on using `SKPaymentQueue` and processing purchases is +/// available at the [In-App Purchase Programming +/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). +class SKPaymentQueueWrapper { + /// Returns the default payment queue. + /// + /// We do not support instantiating a custom payment queue, hence the + /// singleton. However, you can override the observer. + factory SKPaymentQueueWrapper() { + return _singleton; + } + + SKPaymentQueueWrapper._(); + + static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); + + SKPaymentQueueDelegateWrapper? _paymentQueueDelegate; + SKTransactionObserverWrapper? _observer; + + /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) + Future> transactions() async { + return _getTransactionList((await channel + .invokeListMethod('-[SKPaymentQueue transactions]'))!); + } + + /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). + static Future canMakePayments() async => + (await channel + .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? + false; + + /// Sets an observer to listen to all incoming transaction events. + /// + /// This should be called and set as soon as the app launches in order to + /// avoid missing any purchase updates from the App Store. See the + /// documentation on StoreKit's [`-[SKPaymentQueue + /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). + void setTransactionObserver(SKTransactionObserverWrapper observer) { + _observer = observer; + channel.setMethodCallHandler(handleObserverCallbacks); + } + + /// Instructs the iOS implementation to register a transaction observer and + /// start listening to it. + /// + /// Call this method when the first listener is subscribed to the + /// [InAppPurchaseStoreKitPlatform.purchaseStream]. + Future startObservingTransactionQueue() => channel + .invokeMethod('-[SKPaymentQueue startObservingTransactionQueue]'); + + /// Instructs the iOS implementation to remove the transaction observer and + /// stop listening to it. + /// + /// Call this when there are no longer any listeners subscribed to the + /// [InAppPurchaseStoreKitPlatform.purchaseStream]. + Future stopObservingTransactionQueue() => channel + .invokeMethod('-[SKPaymentQueue stopObservingTransactionQueue]'); + + /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. + /// + /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to + /// finish transactions when the storefront changes or if the price consent + /// sheet should be displayed when the price of a subscription has changed. If + /// no delegate is registered iOS will fallback to it's default configuration. + /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). + /// + /// When set to `null` the payment queue delegate will be removed and the + /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). + Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) async { + if (delegate == null) { + await channel.invokeMethod('-[SKPaymentQueue removeDelegate]'); + paymentQueueDelegateChannel.setMethodCallHandler(null); + } else { + await channel.invokeMethod('-[SKPaymentQueue registerDelegate]'); + paymentQueueDelegateChannel + .setMethodCallHandler(handlePaymentQueueDelegateCallbacks); + } + + _paymentQueueDelegate = delegate; + } + + /// Posts a payment to the queue. + /// + /// This sends a purchase request to the App Store for confirmation. + /// Transaction updates will be delivered to the set + /// [SkTransactionObserverWrapper]. + /// + /// A couple preconditions need to be met before calling this method. + /// + /// - At least one [SKTransactionObserverWrapper] should have been added to + /// the payment queue using [addTransactionObserver]. + /// - The [payment.productIdentifier] needs to have been previously fetched + /// using [SKRequestMaker.startProductRequest] so that a valid `SKProduct` + /// has been cached in the platform side already. Because of this + /// [payment.productIdentifier] cannot be hardcoded. + /// + /// This method calls StoreKit's [`-[SKPaymentQueue addPayment:]`] + /// (https://developer.apple.com/documentation/storekit/skpaymentqueue/1506036-addpayment?preferredLanguage=occ). + /// + /// Also see [sandbox + /// testing](https://developer.apple.com/apple-pay/sandbox-testing/). + Future addPayment(SKPaymentWrapper payment) async { + assert(_observer != null, + '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); + final Map requestMap = payment.toMap(); + await channel.invokeMethod( + '-[InAppPurchasePlugin addPayment:result:]', + requestMap, + ); + } + + /// Finishes a transaction and removes it from the queue. + /// + /// This method should be called after the given [transaction] has been + /// succesfully processed and its content has been delivered to the user. + /// Transaction status updates are propagated to [SkTransactionObserver]. + /// + /// This will throw a Platform exception if [transaction.transactionState] is + /// [SKPaymentTransactionStateWrapper.purchasing]. + /// + /// This method calls StoreKit's [`-[SKPaymentQueue + /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). + Future finishTransaction( + SKPaymentTransactionWrapper transaction) async { + final Map requestMap = transaction.toFinishMap(); + await channel.invokeMethod( + '-[InAppPurchasePlugin finishTransaction:result:]', + requestMap, + ); + } + + /// Restore previously purchased transactions. + /// + /// Use this to load previously purchased content on a new device. + /// + /// This call triggers purchase updates on the set + /// [SKTransactionObserverWrapper] for previously made transactions. This will + /// invoke [SKTransactionObserverWrapper.restoreCompletedTransactions], + /// [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished], + /// and [SKTransactionObserverWrapper.updatedTransaction]. These restored + /// transactions need to be marked complete with [finishTransaction] once the + /// content is delivered, like any other transaction. + /// + /// The `applicationUserName` should match the original + /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. + /// If no `applicationUserName` was used, `applicationUserName` should be null. + /// + /// This method either triggers [`-[SKPayment + /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) + /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) + /// depending on whether the `applicationUserName` is set. + Future restoreTransactions({String? applicationUserName}) async { + await channel.invokeMethod( + '-[InAppPurchasePlugin restoreTransactions:result:]', + applicationUserName); + } + + /// Present Code Redemption Sheet + /// + /// Use this to allow Users to enter and redeem Codes + /// + /// This method triggers [`-[SKPayment + /// presentCodeRedemptionSheet]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3566726-presentcoderedemptionsheet?language=objc) + Future presentCodeRedemptionSheet() async { + await channel.invokeMethod( + '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]'); + } + + /// Shows the price consent sheet if the user has not yet responded to a + /// subscription price change. + /// + /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] + /// (using the [setDelegate] method) and returned `false` when the + /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. + /// + /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). + Future showPriceConsentIfNeeded() async { + await channel + .invokeMethod('-[SKPaymentQueue showPriceConsentIfNeeded]'); + } + + /// Triage a method channel call from the platform and triggers the correct observer method. + /// + /// This method is public for testing purposes only and should not be used + /// outside this class. + @visibleForTesting + Future handleObserverCallbacks(MethodCall call) async { + assert(_observer != null, + '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); + final SKTransactionObserverWrapper observer = _observer!; + switch (call.method) { + case 'updatedTransactions': + { + final List transactions = + _getTransactionList(call.arguments as List); + return Future(() { + observer.updatedTransactions(transactions: transactions); + }); + } + case 'removedTransactions': + { + final List transactions = + _getTransactionList(call.arguments as List); + return Future(() { + observer.removedTransactions(transactions: transactions); + }); + } + case 'restoreCompletedTransactionsFailed': + { + final SKError error = SKError.fromJson(Map.from( + call.arguments as Map)); + return Future(() { + observer.restoreCompletedTransactionsFailed(error: error); + }); + } + case 'paymentQueueRestoreCompletedTransactionsFinished': + { + return Future(() { + observer.paymentQueueRestoreCompletedTransactionsFinished(); + }); + } + case 'shouldAddStorePayment': + { + final SKPaymentWrapper payment = SKPaymentWrapper.fromJson( + (call.arguments['payment'] as Map) + .cast()); + final SKProductWrapper product = SKProductWrapper.fromJson( + (call.arguments['product'] as Map) + .cast()); + return Future(() { + if (observer.shouldAddStorePayment( + payment: payment, product: product) == + true) { + SKPaymentQueueWrapper().addPayment(payment); + } + }); + } + default: + break; + } + throw PlatformException( + code: 'no_such_callback', + message: 'Did not recognize the observer callback ${call.method}.'); + } + + // Get transaction wrapper object list from arguments. + List _getTransactionList( + List transactionsData) { + return transactionsData.map((dynamic map) { + return SKPaymentTransactionWrapper.fromJson( + Map.castFrom( + map as Map)); + }).toList(); + } + + /// Triage a method channel call from the platform and triggers the correct + /// payment queue delegate method. + /// + /// This method is public for testing purposes only and should not be used + /// outside this class. + @visibleForTesting + Future handlePaymentQueueDelegateCallbacks(MethodCall call) async { + assert(_paymentQueueDelegate != null, + '[in_app_purchase]: (Fatal)The payment queue delegate has not been set but we received a payment queue notification. Please ensure the payment queue has been set using `setDelegate`.'); + + final SKPaymentQueueDelegateWrapper delegate = _paymentQueueDelegate!; + switch (call.method) { + case 'shouldContinueTransaction': + final SKPaymentTransactionWrapper transaction = + SKPaymentTransactionWrapper.fromJson( + (call.arguments['transaction'] as Map) + .cast()); + final SKStorefrontWrapper storefront = SKStorefrontWrapper.fromJson( + (call.arguments['storefront'] as Map) + .cast()); + return delegate.shouldContinueTransaction(transaction, storefront); + case 'shouldShowPriceConsent': + return delegate.shouldShowPriceConsent(); + default: + break; + } + throw PlatformException( + code: 'no_such_callback', + message: + 'Did not recognize the payment queue delegate callback ${call.method}.'); + } +} + +/// Dart wrapper around StoreKit's +/// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). +@immutable +@JsonSerializable() +class SKError { + /// Creates a new [SKError] object with the provided information. + const SKError( + {required this.code, required this.domain, required this.userInfo}); + + /// Constructs an instance of this from a key-value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKError.fromJson(Map map) { + return _$SKErrorFromJson(map); + } + + /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) + /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: 0) + final int code; + + /// Error + /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) + /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: '') + final String domain; + + /// A map that contains more detailed information about the error. + /// + /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). + @JsonKey(defaultValue: {}) + final Map userInfo; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is SKError && + other.code == code && + other.domain == domain && + const DeepCollectionEquality.unordered() + .equals(other.userInfo, userInfo); + } + + @override + int get hashCode => Object.hash( + code, + domain, + userInfo, + ); +} + +/// Dart wrapper around StoreKit's +/// [SKPayment](https://developer.apple.com/documentation/storekit/skpayment?language=objc). +/// +/// Used as the parameter to initiate a payment. In general, a developer should +/// not need to create the payment object explicitly; instead, use +/// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to +/// initiate a payment. +@immutable +@JsonSerializable(createToJson: true) +class SKPaymentWrapper { + /// Creates a new [SKPaymentWrapper] with the provided information. + const SKPaymentWrapper({ + required this.productIdentifier, + this.applicationUsername, + this.requestData, + this.quantity = 1, + this.simulatesAskToBuyInSandbox = false, + this.paymentDiscount, + }); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKPaymentWrapper.fromJson(Map map) { + assert(map != null); + return _$SKPaymentWrapperFromJson(map); + } + + /// Creates a Map object describes the payment object. + Map toMap() { + return { + 'productIdentifier': productIdentifier, + 'applicationUsername': applicationUsername, + 'requestData': requestData, + 'quantity': quantity, + 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox + }; + } + + /// The id for the product that the payment is for. + @JsonKey(defaultValue: '') + final String productIdentifier; + + /// An opaque id for the user's account. + /// + /// Used to help the store detect irregular activity. See + /// [applicationUsername](https://developer.apple.com/documentation/storekit/skpayment/1506116-applicationusername?language=objc) + /// for more details. For example, you can use a one-way hash of the user’s + /// account name on your server. Don’t use the Apple ID for your developer + /// account, the user’s Apple ID, or the user’s plaintext account name on + /// your server. + final String? applicationUsername; + + /// Reserved for future use. + /// + /// The value must be null before sending the payment. If the value is not + /// null, the payment will be rejected. + /// + // The iOS Platform provided this property but it is reserved for future use. + // We also provide this property to match the iOS platform. Converted to + // String from NSData from ios platform using UTF8Encoding. The / default is + // null. + final String? requestData; + + /// The amount of the product this payment is for. + /// + /// The default is 1. The minimum is 1. The maximum is 10. + /// + /// If the object is invalid, the value could be 0. + @JsonKey(defaultValue: 0) + final int quantity; + + /// Produces an "ask to buy" flow in the sandbox. + /// + /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], + /// which produce an "ask to buy" prompt that interrupts the the payment flow. + /// + /// Default is `false`. + /// + /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox + /// testing. + final bool simulatesAskToBuyInSandbox; + + /// The details of a discount that should be applied to the payment. + /// + /// See [Implementing Promotional Offers in Your App](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app?language=objc) + /// for more information on generating keys and creating offers for + /// auto-renewable subscriptions. If set to `null` no discount will be + /// applied to this payment. + final SKPaymentDiscountWrapper? paymentDiscount; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is SKPaymentWrapper && + other.productIdentifier == productIdentifier && + other.applicationUsername == applicationUsername && + other.quantity == quantity && + other.simulatesAskToBuyInSandbox == simulatesAskToBuyInSandbox && + other.requestData == requestData; + } + + @override + int get hashCode => Object.hash(productIdentifier, applicationUsername, + quantity, simulatesAskToBuyInSandbox, requestData); + + @override + String toString() => _$SKPaymentWrapperToJson(this).toString(); +} + +/// Dart wrapper around StoreKit's +/// [SKPaymentDiscount](https://developer.apple.com/documentation/storekit/skpaymentdiscount?language=objc). +/// +/// Used to indicate a discount is applicable to a payment. The +/// [SKPaymentDiscountWrapper] instance should be assigned to the +/// [SKPaymentWrapper] object to which the discount should be applied. +/// Discount offers are set up in App Store Connect. See [Implementing Promotional Offers in Your App](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app?language=objc) +/// for more information. +@immutable +@JsonSerializable(createToJson: true) +class SKPaymentDiscountWrapper { + /// Creates a new [SKPaymentDiscountWrapper] with the provided information. + const SKPaymentDiscountWrapper({ + required this.identifier, + required this.keyIdentifier, + required this.nonce, + required this.signature, + required this.timestamp, + }); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory SKPaymentDiscountWrapper.fromJson(Map map) { + assert(map != null); + return _$SKPaymentDiscountWrapperFromJson(map); + } + + /// Creates a Map object describes the payment object. + Map toMap() { + return { + 'identifier': identifier, + 'keyIdentifier': keyIdentifier, + 'nonce': nonce, + 'signature': signature, + 'timestamp': timestamp, + }; + } + + /// The identifier of the discount offer. + /// + /// The identifier must match one of the offers set up in App Store Connect. + final String identifier; + + /// A string identifying the key that is used to generate the signature. + /// + /// Keys are generated and downloaded from App Store Connect. See + /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final String keyIdentifier; + + /// A universal unique identifier (UUID) created together with the signature. + /// + /// The UUID should be generated on your server when it creates the + /// `signature` for the payment discount. The UUID can be used once, a new + /// UUID should be created for each payment request. The string representation + /// of the UUID must be lowercase. See + /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final String nonce; + + /// A cryptographically signed string representing the to properties of the + /// promotional offer. + /// + /// The signature is string signed with a private key and contains all the + /// properties of the promotional offer. To keep you private key secure the + /// signature should be created on a server. See [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final String signature; + + /// The date and time the signature was created. + /// + /// The timestamp should be formatted in Unix epoch time. See + /// [Generating a Signature for Promotional Offers](https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers?language=objc) + /// for more information. + final int timestamp; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is SKPaymentDiscountWrapper && + other.identifier == identifier && + other.keyIdentifier == keyIdentifier && + other.nonce == nonce && + other.signature == signature && + other.timestamp == timestamp; + } + + @override + int get hashCode => + 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_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart new file mode 100644 index 000000000000..f594ad450440 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_payment_queue_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKError _$SKErrorFromJson(Map json) => SKError( + code: json['code'] as int? ?? 0, + domain: json['domain'] as String? ?? '', + userInfo: (json['userInfo'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {}, + ); + +SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) => SKPaymentWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + applicationUsername: json['applicationUsername'] as String?, + requestData: json['requestData'] as String?, + quantity: json['quantity'] as int? ?? 0, + simulatesAskToBuyInSandbox: + json['simulatesAskToBuyInSandbox'] as bool? ?? false, + ); + +Map _$SKPaymentWrapperToJson(SKPaymentWrapper instance) => + { + 'productIdentifier': instance.productIdentifier, + 'applicationUsername': instance.applicationUsername, + 'requestData': instance.requestData, + 'quantity': instance.quantity, + 'simulatesAskToBuyInSandbox': instance.simulatesAskToBuyInSandbox, + }; + +SKPaymentDiscountWrapper _$SKPaymentDiscountWrapperFromJson(Map json) => + SKPaymentDiscountWrapper( + identifier: json['identifier'] as String, + keyIdentifier: json['keyIdentifier'] as String, + nonce: json['nonce'] as String, + signature: json['signature'] as String, + timestamp: json['timestamp'] as int, + ); + +Map _$SKPaymentDiscountWrapperToJson( + SKPaymentDiscountWrapper instance) => + { + 'identifier': instance.identifier, + 'keyIdentifier': instance.keyIdentifier, + 'nonce': instance.nonce, + 'signature': instance.signature, + 'timestamp': instance.timestamp, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 88% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 01cd6db0dda1..3894721a1f80 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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,11 +2,12 @@ // 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'; -import 'sk_product_wrapper.dart'; -import 'sk_payment_queue_wrapper.dart'; + import 'enum_converters.dart'; +import 'sk_payment_queue_wrapper.dart'; +import 'sk_product_wrapper.dart'; part 'sk_payment_transaction_wrappers.g.dart'; @@ -101,9 +102,13 @@ enum SKPaymentTransactionStateWrapper { /// /// Dart wrapper around StoreKit's /// [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc). -@JsonSerializable() +@JsonSerializable(createToJson: true) +@immutable class SKPaymentTransactionWrapper { /// Creates a new [SKPaymentTransactionWrapper] with the provided information. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SKPaymentTransactionWrapper({ required this.payment, required this.transactionState, @@ -173,31 +178,25 @@ class SKPaymentTransactionWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentTransactionWrapper typedOther = - other as SKPaymentTransactionWrapper; - return typedOther.payment == payment && - typedOther.transactionState == transactionState && - typedOther.originalTransaction == originalTransaction && - typedOther.transactionTimeStamp == transactionTimeStamp && - typedOther.transactionIdentifier == transactionIdentifier && - typedOther.error == error; + return other is SKPaymentTransactionWrapper && + other.payment == payment && + other.transactionState == transactionState && + other.originalTransaction == originalTransaction && + other.transactionTimeStamp == transactionTimeStamp && + other.transactionIdentifier == transactionIdentifier && + other.error == error; } @override - int get hashCode => hashValues( - this.payment, - this.transactionState, - this.originalTransaction, - this.transactionTimeStamp, - this.transactionIdentifier, - this.error); + int get hashCode => Object.hash(payment, transactionState, + originalTransaction, transactionTimeStamp, transactionIdentifier, error); @override String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); /// The payload that is used to finish this transaction. Map toFinishMap() => { - "transactionIdentifier": this.transactionIdentifier, - "productIdentifier": this.payment.productIdentifier, + 'transactionIdentifier': transactionIdentifier, + 'productIdentifier': payment.productIdentifier, }; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart new file mode 100644 index 000000000000..fd10d9ad977b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_payment_transaction_wrappers.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) => + SKPaymentTransactionWrapper( + payment: SKPaymentWrapper.fromJson( + Map.from(json['payment'] as Map)), + transactionState: const SKTransactionStatusConverter() + .fromJson(json['transactionState'] as int?), + originalTransaction: json['originalTransaction'] == null + ? null + : SKPaymentTransactionWrapper.fromJson( + Map.from(json['originalTransaction'] as Map)), + transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), + transactionIdentifier: json['transactionIdentifier'] as String?, + error: json['error'] == null + ? null + : SKError.fromJson(Map.from(json['error'] as Map)), + ); + +Map _$SKPaymentTransactionWrapperToJson( + SKPaymentTransactionWrapper instance) => + { + 'transactionState': const SKTransactionStatusConverter() + .toJson(instance.transactionState), + 'payment': instance.payment, + 'originalTransaction': instance.originalTransaction, + 'transactionTimeStamp': instance.transactionTimeStamp, + 'transactionIdentifier': instance.transactionIdentifier, + 'error': instance.error, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 78% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 1b681f24f8db..2354563261fc 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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,8 +2,8 @@ // 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'; import 'enum_converters.dart'; @@ -17,8 +17,12 @@ part 'sk_product_wrapper.g.dart'; /// Represents the response object returned by [SKRequestMaker.startProductRequest]. /// Contains information about a list of products and a list of invalid product identifiers. @JsonSerializable() +@immutable class SkProductResponseWrapper { /// Creates an [SkProductResponseWrapper] with the given product details. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SkProductResponseWrapper( {required this.products, required this.invalidProductIdentifiers}); @@ -52,15 +56,14 @@ class SkProductResponseWrapper { if (other.runtimeType != runtimeType) { return false; } - final SkProductResponseWrapper typedOther = - other as SkProductResponseWrapper; - return DeepCollectionEquality().equals(typedOther.products, products) && - DeepCollectionEquality().equals( - typedOther.invalidProductIdentifiers, invalidProductIdentifiers); + return other is SkProductResponseWrapper && + const DeepCollectionEquality().equals(other.products, products) && + const DeepCollectionEquality() + .equals(other.invalidProductIdentifiers, invalidProductIdentifiers); } @override - int get hashCode => hashValues(this.products, this.invalidProductIdentifiers); + int get hashCode => Object.hash(products, invalidProductIdentifiers); } /// Dart wrapper around StoreKit's [SKProductPeriodUnit](https://developer.apple.com/documentation/storekit/skproductperiodunit?language=objc). @@ -93,8 +96,12 @@ enum SKSubscriptionPeriodUnit { /// A period is defined by a [numberOfUnits] and a [unit], e.g for a 3 months period [numberOfUnits] is 3 and [unit] is a month. /// It is used as a property in [SKProductDiscountWrapper] and [SKProductWrapper]. @JsonSerializable() +@immutable class SKProductSubscriptionPeriodWrapper { /// Creates an [SKProductSubscriptionPeriodWrapper] for a `numberOfUnits`x`unit` period. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SKProductSubscriptionPeriodWrapper( {required this.numberOfUnits, required this.unit}); @@ -128,13 +135,13 @@ class SKProductSubscriptionPeriodWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductSubscriptionPeriodWrapper typedOther = - other as SKProductSubscriptionPeriodWrapper; - return typedOther.numberOfUnits == numberOfUnits && typedOther.unit == unit; + return other is SKProductSubscriptionPeriodWrapper && + other.numberOfUnits == numberOfUnits && + other.unit == unit; } @override - int get hashCode => hashValues(this.numberOfUnits, this.unit); + int get hashCode => Object.hash(numberOfUnits, unit); } /// Dart wrapper around StoreKit's [SKProductDiscountPaymentMode](https://developer.apple.com/documentation/storekit/skproductdiscountpaymentmode?language=objc). @@ -164,8 +171,12 @@ enum SKProductDiscountPaymentMode { /// /// It is used as a property in [SKProductWrapper]. @JsonSerializable() +@immutable class SKProductDiscountWrapper { /// Creates an [SKProductDiscountWrapper] with the given discount details. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SKProductDiscountWrapper( {required this.price, required this.priceLocale, @@ -211,18 +222,17 @@ class SKProductDiscountWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductDiscountWrapper typedOther = - other as SKProductDiscountWrapper; - return typedOther.price == price && - typedOther.priceLocale == priceLocale && - typedOther.numberOfPeriods == numberOfPeriods && - typedOther.paymentMode == paymentMode && - typedOther.subscriptionPeriod == subscriptionPeriod; + return other is SKProductDiscountWrapper && + other.price == price && + other.priceLocale == priceLocale && + other.numberOfPeriods == numberOfPeriods && + other.paymentMode == paymentMode && + other.subscriptionPeriod == subscriptionPeriod; } @override - int get hashCode => hashValues(this.price, this.priceLocale, - this.numberOfPeriods, this.paymentMode, this.subscriptionPeriod); + int get hashCode => Object.hash( + price, priceLocale, numberOfPeriods, paymentMode, subscriptionPeriod); } /// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). @@ -230,8 +240,12 @@ class SKProductDiscountWrapper { /// A list of [SKProductWrapper] is returned in the [SKRequestMaker.startProductRequest] method, and /// should be stored for use when making a payment. @JsonSerializable() +@immutable class SKProductWrapper { /// Creates an [SKProductWrapper] with the given product details. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SKProductWrapper({ required this.productIdentifier, required this.localizedTitle, @@ -241,6 +255,7 @@ class SKProductWrapper { required this.price, this.subscriptionPeriod, this.introductoryPrice, + this.discounts = const [], }); /// Constructing an instance from a map from the Objective-C layer. @@ -295,6 +310,16 @@ class SKProductWrapper { /// and their units and duration do not have to be matched. final SKProductDiscountWrapper? introductoryPrice; + /// An array of subscription offers available for the auto-renewable subscription (available on iOS 12.2 and higher). + /// + /// This property lists all promotional offers set up in App Store Connect. If + /// no promotional offers have been set up, this field returns an empty list. + /// Each [subscriptionPeriod] of individual discounts are independent of the + /// product's [subscriptionPeriod] and their units and duration do not have to + /// be matched. + @JsonKey(defaultValue: []) + final List discounts; + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -303,27 +328,29 @@ class SKProductWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductWrapper typedOther = other as SKProductWrapper; - return typedOther.productIdentifier == productIdentifier && - typedOther.localizedTitle == localizedTitle && - typedOther.localizedDescription == localizedDescription && - typedOther.priceLocale == priceLocale && - typedOther.subscriptionGroupIdentifier == subscriptionGroupIdentifier && - typedOther.price == price && - typedOther.subscriptionPeriod == subscriptionPeriod && - typedOther.introductoryPrice == introductoryPrice; + return other is SKProductWrapper && + other.productIdentifier == productIdentifier && + other.localizedTitle == localizedTitle && + other.localizedDescription == localizedDescription && + other.priceLocale == priceLocale && + other.subscriptionGroupIdentifier == subscriptionGroupIdentifier && + other.price == price && + other.subscriptionPeriod == subscriptionPeriod && + other.introductoryPrice == introductoryPrice && + const DeepCollectionEquality().equals(other.discounts, discounts); } @override - int get hashCode => hashValues( - this.productIdentifier, - this.localizedTitle, - this.localizedDescription, - this.priceLocale, - this.subscriptionGroupIdentifier, - this.price, - this.subscriptionPeriod, - this.introductoryPrice); + int get hashCode => Object.hash( + productIdentifier, + localizedTitle, + localizedDescription, + priceLocale, + subscriptionGroupIdentifier, + price, + subscriptionPeriod, + introductoryPrice, + discounts); } /// Object that indicates the locale of the price @@ -333,8 +360,12 @@ class SKProductWrapper { // Matching android to only get the currencySymbol for now. // https://github.com/flutter/flutter/issues/26610 @JsonSerializable() +@immutable class SKPriceLocaleWrapper { /// Creates a new price locale for `currencySymbol` and `currencyCode`. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SKPriceLocaleWrapper({ required this.currencySymbol, required this.currencyCode, @@ -372,11 +403,11 @@ class SKPriceLocaleWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPriceLocaleWrapper typedOther = other as SKPriceLocaleWrapper; - return typedOther.currencySymbol == currencySymbol && - typedOther.currencyCode == currencyCode; + return other is SKPriceLocaleWrapper && + other.currencySymbol == currencySymbol && + other.currencyCode == currencyCode; } @override - int get hashCode => hashValues(this.currencySymbol, this.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 new file mode 100644 index 000000000000..6eea3ff34da0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_product_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) => + SkProductResponseWrapper( + products: (json['products'] as List?) + ?.map((e) => SKProductWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + invalidProductIdentifiers: + (json['invalidProductIdentifiers'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + ); + +SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( + Map json) => + SKProductSubscriptionPeriodWrapper( + numberOfUnits: json['numberOfUnits'] as int? ?? 0, + unit: const SKSubscriptionPeriodUnitConverter() + .fromJson(json['unit'] as int?), + ); + +SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) => + SKProductDiscountWrapper( + price: json['price'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, + paymentMode: const SKProductDiscountPaymentModeConverter() + .fromJson(json['paymentMode'] as int?), + subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + ); + +SKProductWrapper _$SKProductWrapperFromJson(Map json) => SKProductWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + localizedTitle: json['localizedTitle'] as String? ?? '', + localizedDescription: json['localizedDescription'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + subscriptionGroupIdentifier: + json['subscriptionGroupIdentifier'] as String?, + price: json['price'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] == null + ? null + : SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + introductoryPrice: json['introductoryPrice'] == null + ? null + : SKProductDiscountWrapper.fromJson( + Map.from(json['introductoryPrice'] as Map)), + discounts: (json['discounts'] as List?) + ?.map((e) => SKProductDiscountWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + ); + +SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) => + SKPriceLocaleWrapper( + currencySymbol: json['currencySymbol'] as String? ?? '', + currencyCode: json['currencyCode'] as String? ?? '', + countryCode: json['countryCode'] as String? ?? '', + ); diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_receipt_manager.dart similarity index 90% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_receipt_manager.dart index 3eb41cb66a14..b31a3d59c172 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -6,7 +6,8 @@ import 'dart:async'; import '../channel.dart'; -///This class contains static methods to manage StoreKit receipts. +// ignore: avoid_classes_with_only_static_members +/// This class contains static methods to manage StoreKit receipts. class SKReceiptManager { /// Retrieve the receipt data from your application's main bundle. /// diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_request_maker.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_request_maker.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 79% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index 934fdea355e3..ff9e9b7db746 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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,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 'package:json_annotation/json_annotation.dart'; part 'sk_storefront_wrapper.g.dart'; @@ -12,9 +11,13 @@ part 'sk_storefront_wrapper.g.dart'; /// /// Dart wrapper around StoreKit's /// [SKStorefront](https://developer.apple.com/documentation/storekit/skstorefront?language=objc). -@JsonSerializable() +@JsonSerializable(createToJson: true) +@immutable class SKStorefrontWrapper { /// Creates a new [SKStorefrontWrapper] with the provided information. + // TODO(stuartmorgan): Temporarily ignore const warning in other parts of the + // federated package, and remove this. + // ignore: prefer_const_constructors_in_immutables SKStorefrontWrapper({ required this.countryCode, required this.identifier, @@ -45,15 +48,15 @@ class SKStorefrontWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKStorefrontWrapper typedOther = other as SKStorefrontWrapper; - return typedOther.countryCode == countryCode && - typedOther.identifier == identifier; + return other is SKStorefrontWrapper && + other.countryCode == countryCode && + other.identifier == identifier; } @override - int get hashCode => hashValues( - this.countryCode, - this.identifier, + int get hashCode => Object.hash( + countryCode, + identifier, ); @override diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart new file mode 100644 index 000000000000..b2d5d3a06d1d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_storefront_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKStorefrontWrapper _$SKStorefrontWrapperFromJson(Map json) => + SKStorefrontWrapper( + countryCode: json['countryCode'] as String, + identifier: json['identifier'] as String, + ); + +Map _$SKStorefrontWrapperToJson( + SKStorefrontWrapper instance) => + { + 'countryCode': instance.countryCode, + 'identifier': instance.identifier, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart index ff1153e27e47..a5d8c7287e3c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart @@ -30,10 +30,6 @@ class AppStoreProductDetails extends ProductDetails { currencySymbol: currencySymbol, ); - /// Points back to the [SKProductWrapper] object that was used to generate - /// this [AppStoreProductDetails] object. - final SKProductWrapper skProduct; - /// Generate a [AppStoreProductDetails] object based on an iOS [SKProductWrapper] object. factory AppStoreProductDetails.fromSKProduct(SKProductWrapper product) { return AppStoreProductDetails( @@ -49,4 +45,8 @@ class AppStoreProductDetails extends ProductDetails { skProduct: product, ); } + + /// Points back to the [SKProductWrapper] object that was used to generate + /// this [AppStoreProductDetails] object. + final SKProductWrapper skProduct; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart similarity index 89% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart index 6d6f241d6ca8..42cb225ede0a 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart @@ -4,7 +4,7 @@ import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; -import '../../in_app_purchase_ios.dart'; +import '../../in_app_purchase_storekit.dart'; import '../../store_kit_wrappers.dart'; import '../store_kit_wrappers/enum_converters.dart'; @@ -29,22 +29,6 @@ class AppStorePurchaseDetails extends PurchaseDetails { this.status = status; } - /// Points back to the [SKPaymentTransactionWrapper] which was used to - /// generate this [AppStorePurchaseDetails] object. - final SKPaymentTransactionWrapper skPaymentTransaction; - - late PurchaseStatus _status; - - /// The status that this [PurchaseDetails] is currently on. - PurchaseStatus get status => _status; - set status(PurchaseStatus status) { - _pendingCompletePurchase = status != PurchaseStatus.pending; - _status = status; - } - - bool _pendingCompletePurchase = false; - bool get pendingCompletePurchase => _pendingCompletePurchase; - /// Generate a [AppStorePurchaseDetails] object based on an iOS /// [SKPaymentTransactionWrapper] object. factory AppStorePurchaseDetails.fromSKTransaction( @@ -55,8 +39,8 @@ class AppStorePurchaseDetails extends PurchaseDetails { productID: transaction.payment.productIdentifier, purchaseID: transaction.transactionIdentifier, skPaymentTransaction: transaction, - status: SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState), + status: const SKTransactionStatusConverter() + .toPurchaseStatus(transaction.transactionState, transaction.error), transactionDate: transaction.transactionTimeStamp != null ? (transaction.transactionTimeStamp! * 1000).toInt().toString() : null, @@ -66,7 +50,8 @@ class AppStorePurchaseDetails extends PurchaseDetails { source: kIAPSource), ); - if (purchaseDetails.status == PurchaseStatus.error) { + if (purchaseDetails.status == PurchaseStatus.error || + purchaseDetails.status == PurchaseStatus.canceled) { purchaseDetails.error = IAPError( source: kIAPSource, code: kPurchaseErrorCode, @@ -77,4 +62,23 @@ class AppStorePurchaseDetails extends PurchaseDetails { return purchaseDetails; } + + /// Points back to the [SKPaymentTransactionWrapper] which was used to + /// generate this [AppStorePurchaseDetails] object. + final SKPaymentTransactionWrapper skPaymentTransaction; + + late PurchaseStatus _status; + + /// The status that this [PurchaseDetails] is currently on. + @override + PurchaseStatus get status => _status; + @override + set status(PurchaseStatus status) { + _pendingCompletePurchase = status != PurchaseStatus.pending; + _status = status; + } + + bool _pendingCompletePurchase = false; + @override + bool get pendingCompletePurchase => _pendingCompletePurchase; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 92% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart index b2d8eea9d791..168ef5cea5f4 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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,6 +12,7 @@ class AppStorePurchaseParam extends PurchaseParam { AppStorePurchaseParam({ required ProductDetails productDetails, String? applicationUserName, + this.quantity = 1, this.simulatesAskToBuyInSandbox = false, }) : super( productDetails: productDetails, @@ -28,4 +29,7 @@ class AppStorePurchaseParam extends PurchaseParam { /// /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. final bool simulatesAskToBuyInSandbox; + + /// Quantity of the product user requested to buy. + final int quantity; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/types.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/types.dart diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/store_kit_wrappers.dart similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart rename to packages/in_app_purchase/in_app_purchase_storekit/lib/store_kit_wrappers.dart diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml new file mode 100644 index 000000000000..dd65b259a283 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -0,0 +1,30 @@ +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.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: in_app_purchase + platforms: + ios: + pluginClass: InAppPurchasePlugin + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.3.0 + json_annotation: ^4.3.0 + +dev_dependencies: + build_runner: ^2.0.0 + flutter_test: + sdk: flutter + json_serializable: ^6.0.0 + test: ^1.16.0 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 new file mode 100644 index 000000000000..e987a5ceac8c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -0,0 +1,221 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; +import 'package:in_app_purchase_storekit/src/channel.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; + +import '../store_kit_wrappers/sk_test_stub_objects.dart'; + +class FakeStoreKitPlatform { + FakeStoreKitPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + + // pre-configured store information + String? receiptData; + late Set validProductIDs; + late Map validProducts; + late List transactions; + late List finishedTransactions; + late bool testRestoredTransactionsNull; + late bool testTransactionFail; + late int testTransactionCancel; + PlatformException? queryProductException; + PlatformException? restoreException; + SKError? testRestoredError; + bool queueIsActive = false; + + void reset() { + transactions = []; + receiptData = 'dummy base64data'; + validProductIDs = {'123', '456'}; + validProducts = {}; + for (final String validID in validProductIDs) { + final Map productWrapperMap = + buildProductMap(dummyProductWrapper); + productWrapperMap['productIdentifier'] = validID; + if (validID == '456') { + productWrapperMap['priceLocale'] = buildLocaleMap(noSymbolLocale); + } + validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); + } + + finishedTransactions = []; + testRestoredTransactionsNull = false; + testTransactionFail = false; + testTransactionCancel = -1; + queryProductException = null; + restoreException = null; + testRestoredError = null; + queueIsActive = false; + } + + SKPaymentTransactionWrapper createPendingTransaction(String id, + {int quantity = 1}) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: id, quantity: quantity), + transactionState: SKPaymentTransactionStateWrapper.purchasing, + transactionTimeStamp: 123123.121, + error: null, + originalTransaction: null, + ); + } + + SKPaymentTransactionWrapper createPurchasedTransaction( + String productId, String transactionId, + {int quantity = 1}) { + return SKPaymentTransactionWrapper( + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), + transactionState: SKPaymentTransactionStateWrapper.purchased, + transactionTimeStamp: 123123.121, + transactionIdentifier: transactionId, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createFailedTransaction(String productId, + {int quantity = 1}) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + 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); + } + + SKPaymentTransactionWrapper createCanceledTransaction( + String productId, int errorCode, + {int quantity = 1}) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + 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); + } + + SKPaymentTransactionWrapper createRestoredTransaction( + String productId, String transactionId, + {int quantity = 1}) { + return SKPaymentTransactionWrapper( + payment: + SKPaymentWrapper(productIdentifier: productId, quantity: quantity), + transactionState: SKPaymentTransactionStateWrapper.restored, + transactionTimeStamp: 123123.121, + transactionIdentifier: transactionId, + error: null, + originalTransaction: null); + } + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case '-[SKPaymentQueue canMakePayments:]': + return Future.value(true); + case '-[InAppPurchasePlugin startProductRequest:result:]': + if (queryProductException != null) { + throw queryProductException!; + } + final List productIDS = + List.castFrom(call.arguments as List); + final List invalidFound = []; + final List products = []; + for (final String productID in productIDS) { + if (!validProductIDs.contains(productID)) { + invalidFound.add(productID); + } else { + products.add(validProducts[productID]!); + } + } + final SkProductResponseWrapper response = SkProductResponseWrapper( + products: products, invalidProductIdentifiers: invalidFound); + return Future>.value( + buildProductResponseMap(response)); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + if (restoreException != null) { + throw restoreException!; + } + if (testRestoredError != null) { + InAppPurchaseStoreKitPlatform.observer + .restoreCompletedTransactionsFailed(error: testRestoredError!); + return Future.sync(() {}); + } + if (!testRestoredTransactionsNull) { + InAppPurchaseStoreKitPlatform.observer + .updatedTransactions(transactions: transactions); + } + InAppPurchaseStoreKitPlatform.observer + .paymentQueueRestoreCompletedTransactionsFinished(); + + return Future.sync(() {}); + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (receiptData != null) { + return Future.value(receiptData); + } else { + throw PlatformException(code: 'no_receipt_data'); + } + case '-[InAppPurchasePlugin refreshReceipt:result:]': + receiptData = 'refreshed receipt data'; + return Future.sync(() {}); + case '-[InAppPurchasePlugin addPayment:result:]': + final String id = call.arguments['productIdentifier'] as String; + final int quantity = call.arguments['quantity'] as int; + final SKPaymentTransactionWrapper transaction = + createPendingTransaction(id, quantity: quantity); + transactions.add(transaction); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transaction]); + sleep(const Duration(milliseconds: 30)); + if (testTransactionFail) { + final SKPaymentTransactionWrapper transactionFailed = + createFailedTransaction(id, quantity: quantity); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transactionFailed]); + } else if (testTransactionCancel > 0) { + final SKPaymentTransactionWrapper transactionCanceled = + createCanceledTransaction(id, testTransactionCancel, + quantity: quantity); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transactionCanceled]); + } else { + final SKPaymentTransactionWrapper transactionFinished = + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? '', + quantity: quantity); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transactionFinished]); + } + break; + case '-[InAppPurchasePlugin finishTransaction:result:]': + finishedTransactions.add(createPurchasedTransaction( + call.arguments['productIdentifier'] as String, + call.arguments['transactionIdentifier'] as String, + quantity: transactions.first.payment.quantity)); + break; + case '-[SKPaymentQueue startObservingTransactionQueue]': + queueIsActive = true; + break; + case '-[SKPaymentQueue stopObservingTransactionQueue]': + queueIsActive = false; + break; + } + return Future.sync(() {}); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart new file mode 100644 index 000000000000..dfdff5117091 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.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/services.dart'; +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_storekit/in_app_purchase_storekit.dart'; + +import 'fakes/fake_storekit_platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + }); + + group('present code redemption sheet', () { + test('null', () async { + expect( + InAppPurchaseStoreKitPlatformAddition().presentCodeRedemptionSheet(), + completes); + }); + }); + + group('refresh receipt data', () { + test('should refresh receipt data', () async { + final PurchaseVerificationData? receiptData = + await InAppPurchaseStoreKitPlatformAddition() + .refreshPurchaseVerificationData(); + expect(receiptData, isNotNull); + expect(receiptData!.source, kIAPSource); + expect(receiptData.localVerificationData, 'refreshed receipt data'); + expect(receiptData.serverVerificationData, 'refreshed receipt data'); + }); + }); +} 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 new file mode 100644 index 000000000000..852599ac3670 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart @@ -0,0 +1,541 @@ +// Copyright 2013 The Flutter Authors. 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: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/src/store_kit_wrappers/enum_converters.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; + +import 'fakes/fake_storekit_platform.dart'; +import 'store_kit_wrappers/sk_test_stub_objects.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); + late InAppPurchaseStoreKitPlatform iapStoreKitPlatform; + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + }); + + setUp(() { + InAppPurchaseStoreKitPlatform.registerPlatform(); + iapStoreKitPlatform = + InAppPurchasePlatform.instance as InAppPurchaseStoreKitPlatform; + fakeStoreKitPlatform.reset(); + }); + + tearDown(() => fakeStoreKitPlatform.reset()); + + group('isAvailable', () { + test('true', () async { + expect(await iapStoreKitPlatform.isAvailable(), isTrue); + }); + }); + + group('query product list', () { + test('should get product list and correct invalid identifiers', () async { + final InAppPurchaseStoreKitPlatform connection = + InAppPurchaseStoreKitPlatform(); + final ProductDetailsResponse response = + await connection.queryProductDetails({'123', '456', '789'}); + final List products = response.productDetails; + expect(products.first.id, '123'); + expect(products[1].id, '456'); + expect(response.notFoundIDs, ['789']); + expect(response.error, isNull); + expect(response.productDetails.first.currencySymbol, r'$'); + expect(response.productDetails[1].currencySymbol, 'EUR'); + }); + + test( + 'if query products throws error, should get error object in the response', + () async { + fakeStoreKitPlatform.queryProductException = PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}); + final InAppPurchaseStoreKitPlatform connection = + InAppPurchaseStoreKitPlatform(); + final ProductDetailsResponse response = + await connection.queryProductDetails({'123', '456', '789'}); + expect(response.productDetails, []); + expect(response.notFoundIDs, ['123', '456', '789']); + expect(response.error, isNotNull); + expect(response.error!.source, kIAPSource); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); + }); + }); + + group('restore purchases', () { + test('should emit restored transactions on purchase stream', () async { + fakeStoreKitPlatform.transactions.insert( + 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1')); + fakeStoreKitPlatform.transactions.insert( + 1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2')); + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + subscription.cancel(); + completer.complete(purchaseDetailsList); + } + }); + + await iapStoreKitPlatform.restorePurchases(); + final List details = await completer.future; + + expect(details.length, 2); + for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) { + final SKPaymentTransactionWrapper expected = + fakeStoreKitPlatform.transactions[i]; + final PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect(actual.status, PurchaseStatus.restored); + expect(actual.verificationData.localVerificationData, + fakeStoreKitPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeStoreKitPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test( + 'should emit empty transaction list on purchase stream when there is nothing to restore', + () async { + fakeStoreKitPlatform.testRestoredTransactionsNull = true; + final Completer?> completer = + Completer?>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + expect(purchaseDetailsList.isEmpty, true); + subscription.cancel(); + completer.complete(); + }); + + await iapStoreKitPlatform.restorePurchases(); + await completer.future; + }); + + test('should not block transaction updates', () async { + fakeStoreKitPlatform.transactions.insert( + 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1')); + fakeStoreKitPlatform.transactions.insert( + 1, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar')); + fakeStoreKitPlatform.transactions.insert( + 2, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2')); + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + if (purchaseDetailsList[1].status == PurchaseStatus.purchased) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + await iapStoreKitPlatform.restorePurchases(); + final List details = await completer.future; + expect(details.length, 3); + for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) { + final SKPaymentTransactionWrapper expected = + fakeStoreKitPlatform.transactions[i]; + final PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect( + actual.status, + const SKTransactionStatusConverter() + .toPurchaseStatus(expected.transactionState, expected.error), + ); + expect(actual.verificationData.localVerificationData, + fakeStoreKitPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeStoreKitPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test( + 'should emit empty transaction if transactions array does not contain a transaction with PurchaseStatus.restored status.', + () async { + fakeStoreKitPlatform.transactions.insert( + 0, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar')); + final Completer>> completer = + Completer>>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + final List> purchaseDetails = + >[]; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + purchaseDetails.add(purchaseDetailsList); + + if (purchaseDetails.length == 2) { + completer.complete(purchaseDetails); + subscription.cancel(); + } + }); + await iapStoreKitPlatform.restorePurchases(); + final List> details = await completer.future; + expect(details.length, 2); + expect(details[0], >[]); + for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) { + final SKPaymentTransactionWrapper expected = + fakeStoreKitPlatform.transactions[i]; + final PurchaseDetails actual = details[1][i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect( + actual.status, + const SKTransactionStatusConverter() + .toPurchaseStatus(expected.transactionState, expected.error), + ); + expect(actual.verificationData.localVerificationData, + fakeStoreKitPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeStoreKitPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test('receipt error should populate null to verificationData.data', + () async { + fakeStoreKitPlatform.transactions.insert( + 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1')); + fakeStoreKitPlatform.transactions.insert( + 1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2')); + fakeStoreKitPlatform.receiptData = null; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + + await iapStoreKitPlatform.restorePurchases(); + final List details = await completer.future; + + for (final PurchaseDetails purchase in details) { + expect(purchase.verificationData.localVerificationData, isEmpty); + expect(purchase.verificationData.serverVerificationData, isEmpty); + } + }); + + test('test restore error', () { + fakeStoreKitPlatform.testRestoredError = const SKError( + code: 123, + domain: 'error_test', + userInfo: {'message': 'errorMessage'}); + + expect( + () => iapStoreKitPlatform.restorePurchases(), + throwsA( + isA() + .having((SKError error) => error.code, 'code', 123) + .having((SKError error) => error.domain, 'domain', 'error_test') + .having((SKError error) => error.userInfo, 'userInfo', + {'message': 'errorMessage'}), + )); + }); + }); + + group('make payment', () { + test( + 'buying non consumable, 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: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + final List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + }); + + test( + 'buying consumable, 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: 'appName'); + await iapStoreKitPlatform.buyConsumable(purchaseParam: purchaseParam); + + final List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + }); + + test('buying consumable, should throw when autoConsume is false', () async { + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + expect( + () => iapStoreKitPlatform.buyConsumable( + purchaseParam: purchaseParam, autoConsume: false), + throwsA(isInstanceOf())); + }); + + test('should get failed purchase status', () async { + fakeStoreKitPlatform.testTransactionFail = true; + final List details = []; + final Completer completer = Completer(); + late IAPError error; + + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { + if (purchaseDetails.status == PurchaseStatus.error) { + error = purchaseDetails.error!; + completer.complete(error); + subscription.cancel(); + } + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + final IAPError completerError = await completer.future; + expect(completerError.code, 'purchase_error'); + expect(completerError.source, kIAPSource); + expect(completerError.message, 'ios_domain'); + expect(completerError.details, + {'message': 'an error message'}); + }); + + test( + 'should get canceled purchase status when error code is SKErrorPaymentCancelled', + () async { + fakeStoreKitPlatform.testTransactionCancel = 2; + 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.status == PurchaseStatus.canceled) { + completer.complete(purchaseDetails.status); + subscription.cancel(); + } + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + final PurchaseStatus purchaseStatus = await completer.future; + expect(purchaseStatus, PurchaseStatus.canceled); + }); + + test( + 'should get canceled purchase status when error code is SKErrorOverlayCancelled', + () async { + fakeStoreKitPlatform.testTransactionCancel = 15; + 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.status == PurchaseStatus.canceled) { + completer.complete(purchaseDetails.status); + subscription.cancel(); + } + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + 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); + }); + }); + + group('complete purchase', () { + test('should complete purchase', () 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 AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + final List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + expect(fakeStoreKitPlatform.finishedTransactions.length, 1); + }); + }); + + group('purchase stream', () { + test('Should only have active queue when purchaseStream has listeners', () { + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + expect(fakeStoreKitPlatform.queueIsActive, false); + final StreamSubscription> subscription1 = + stream.listen((List event) {}); + expect(fakeStoreKitPlatform.queueIsActive, true); + final StreamSubscription> subscription2 = + stream.listen((List event) {}); + expect(fakeStoreKitPlatform.queueIsActive, true); + subscription1.cancel(); + expect(fakeStoreKitPlatform.queueIsActive, true); + subscription2.cancel(); + expect(fakeStoreKitPlatform.queueIsActive, false); + }); + }); +} 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 new file mode 100644 index 000000000000..2baf20892ab6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -0,0 +1,305 @@ +// Copyright 2013 The Flutter Authors. 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:in_app_purchase_storekit/src/channel.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; +import 'sk_test_stub_objects.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + }); + + setUp(() {}); + + tearDown(() { + fakeStoreKitPlatform.testReturnNull = false; + fakeStoreKitPlatform.queueIsActive = null; + fakeStoreKitPlatform.getReceiptFailTest = false; + }); + + group('sk_request_maker', () { + test('get products method channel', () async { + final SkProductResponseWrapper productResponseWrapper = + await SKRequestMaker().startProductRequest(['xxx']); + expect( + productResponseWrapper.products, + isNotEmpty, + ); + expect( + productResponseWrapper.products.first.priceLocale.currencySymbol, + r'$', + ); + + expect( + productResponseWrapper.products.first.priceLocale.currencySymbol, + isNot('A'), + ); + expect( + productResponseWrapper.products.first.priceLocale.currencyCode, + 'USD', + ); + expect( + productResponseWrapper.products.first.priceLocale.countryCode, + 'US', + ); + expect( + productResponseWrapper.invalidProductIdentifiers, + isNotEmpty, + ); + + expect( + fakeStoreKitPlatform.startProductRequestParam, + ['xxx'], + ); + }); + + test('get products method channel should throw exception', () async { + fakeStoreKitPlatform.getProductRequestFailTest = true; + expect( + SKRequestMaker().startProductRequest(['xxx']), + throwsException, + ); + fakeStoreKitPlatform.getProductRequestFailTest = false; + }); + + test('refreshed receipt', () async { + final int receiptCountBefore = fakeStoreKitPlatform.refreshReceipt; + await SKRequestMaker().startRefreshReceiptRequest( + receiptProperties: {'isExpired': true}); + expect(fakeStoreKitPlatform.refreshReceipt, receiptCountBefore + 1); + expect(fakeStoreKitPlatform.refreshReceiptParam, + {'isExpired': true}); + }); + + test('should get null receipt if any exceptions are raised', () async { + fakeStoreKitPlatform.getReceiptFailTest = true; + expect(() async => SKReceiptManager.retrieveReceiptData(), + throwsA(const TypeMatcher())); + }); + }); + + group('sk_receipt_manager', () { + test('should get receipt (faking it by returning a `receipt data` string)', + () async { + final String receiptData = await SKReceiptManager.retrieveReceiptData(); + expect(receiptData, 'receipt data'); + }); + }); + + group('sk_payment_queue', () { + test('canMakePayment should return true', () async { + expect(await SKPaymentQueueWrapper.canMakePayments(), true); + }); + + test('canMakePayment returns false if method channel returns null', + () async { + fakeStoreKitPlatform.testReturnNull = true; + expect(await SKPaymentQueueWrapper.canMakePayments(), false); + }); + + test('transactions should return a valid list of transactions', () async { + expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); + }); + + test( + 'throws if observer is not set for payment queue before adding payment', + () async { + expect(SKPaymentQueueWrapper().addPayment(dummyPayment), + throwsAssertionError); + }); + + test('should add payment to the payment queue', () async { + final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + final TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.addPayment(dummyPayment); + expect(fakeStoreKitPlatform.payments.first, equals(dummyPayment)); + }); + + test('should finish transaction', () async { + final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + final TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.finishTransaction(dummyTransaction); + expect(fakeStoreKitPlatform.transactionsFinished.first, + equals(dummyTransaction.toFinishMap())); + }); + + test('should restore transaction', () async { + final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + final TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.restoreTransactions(applicationUserName: 'aUserID'); + expect(fakeStoreKitPlatform.applicationNameHasTransactionRestored, + 'aUserID'); + }); + + test('startObservingTransactionQueue should call methodChannel', () async { + expect(fakeStoreKitPlatform.queueIsActive, isNot(true)); + await SKPaymentQueueWrapper().startObservingTransactionQueue(); + expect(fakeStoreKitPlatform.queueIsActive, true); + }); + + test('stopObservingTransactionQueue should call methodChannel', () async { + expect(fakeStoreKitPlatform.queueIsActive, isNot(false)); + await SKPaymentQueueWrapper().stopObservingTransactionQueue(); + expect(fakeStoreKitPlatform.queueIsActive, false); + }); + + test('setDelegate should call methodChannel', () async { + expect(fakeStoreKitPlatform.isPaymentQueueDelegateRegistered, false); + await SKPaymentQueueWrapper().setDelegate(TestPaymentQueueDelegate()); + expect(fakeStoreKitPlatform.isPaymentQueueDelegateRegistered, true); + await SKPaymentQueueWrapper().setDelegate(null); + expect(fakeStoreKitPlatform.isPaymentQueueDelegateRegistered, false); + }); + + test('showPriceConsentIfNeeded should call methodChannel', () async { + expect(fakeStoreKitPlatform.showPriceConsentIfNeeded, false); + await SKPaymentQueueWrapper().showPriceConsentIfNeeded(); + expect(fakeStoreKitPlatform.showPriceConsentIfNeeded, true); + }); + }); + + group('Code Redemption Sheet', () { + test('presentCodeRedemptionSheet should not throw', () async { + expect(fakeStoreKitPlatform.presentCodeRedemption, false); + await SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + expect(fakeStoreKitPlatform.presentCodeRedemption, true); + fakeStoreKitPlatform.presentCodeRedemption = false; + }); + }); +} + +class FakeStoreKitPlatform { + FakeStoreKitPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + // get product request + List startProductRequestParam = []; + bool getProductRequestFailTest = false; + bool testReturnNull = false; + + // get receipt request + bool getReceiptFailTest = false; + + // refresh receipt request + int refreshReceipt = 0; + late Map refreshReceiptParam; + + // payment queue + List payments = []; + List> transactionsFinished = >[]; + String applicationNameHasTransactionRestored = ''; + + // present Code Redemption + bool presentCodeRedemption = false; + + // show price consent sheet + bool showPriceConsentIfNeeded = false; + + // indicate if the payment queue delegate is registered + bool isPaymentQueueDelegateRegistered = false; + + // Listen to purchase updates + bool? queueIsActive; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + // request makers + case '-[InAppPurchasePlugin startProductRequest:result:]': + startProductRequestParam = call.arguments as List; + if (getProductRequestFailTest) { + return Future.value(null); + } + return Future>.value( + buildProductResponseMap(dummyProductResponseWrapper)); + case '-[InAppPurchasePlugin refreshReceipt:result:]': + refreshReceipt++; + refreshReceiptParam = Map.castFrom( + call.arguments as Map); + return Future.sync(() {}); + // receipt manager + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (getReceiptFailTest) { + throw 'some arbitrary error'; + } + return Future.value('receipt data'); + // payment queue + case '-[SKPaymentQueue canMakePayments:]': + if (testReturnNull) { + return Future.value(null); + } + return Future.value(true); + case '-[SKPaymentQueue transactions]': + return Future>.value( + [buildTransactionMap(dummyTransaction)]); + case '-[InAppPurchasePlugin addPayment:result:]': + payments.add(SKPaymentWrapper.fromJson(Map.from( + call.arguments as Map))); + return Future.sync(() {}); + case '-[InAppPurchasePlugin finishTransaction:result:]': + transactionsFinished.add( + Map.from(call.arguments as Map)); + return Future.sync(() {}); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + applicationNameHasTransactionRestored = call.arguments as String; + return Future.sync(() {}); + case '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]': + presentCodeRedemption = true; + return Future.sync(() {}); + case '-[SKPaymentQueue startObservingTransactionQueue]': + queueIsActive = true; + return Future.sync(() {}); + case '-[SKPaymentQueue stopObservingTransactionQueue]': + queueIsActive = false; + return Future.sync(() {}); + case '-[SKPaymentQueue registerDelegate]': + isPaymentQueueDelegateRegistered = true; + return Future.sync(() {}); + case '-[SKPaymentQueue removeDelegate]': + isPaymentQueueDelegateRegistered = false; + return Future.sync(() {}); + case '-[SKPaymentQueue showPriceConsentIfNeeded]': + showPriceConsentIfNeeded = true; + return Future.sync(() {}); + } + return Future.error('method not mocked'); + } +} + +class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {} + +class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { + @override + void updatedTransactions( + {required List transactions}) {} + + @override + void removedTransactions( + {required List transactions}) {} + + @override + void restoreCompletedTransactionsFailed({required SKError error}) {} + + @override + void paymentQueueRestoreCompletedTransactionsFinished() {} + + @override + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + return true; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart similarity index 78% rename from packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart rename to packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart index ca2b3364d680..df76254aabf7 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart @@ -4,24 +4,24 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase_ios/src/channel.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/src/channel.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { SystemChannels.platform - .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); }); test( 'handlePaymentQueueDelegateCallbacks should call SKPaymentQueueDelegateWrapper.shouldContinueTransaction', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); + final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + final TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); await queue.setDelegate(testDelegate); final Map arguments = { @@ -36,7 +36,7 @@ void main() { }, }; - final result = await queue.handlePaymentQueueDelegateCallbacks( + final Object? result = await queue.handlePaymentQueueDelegateCallbacks( MethodCall('shouldContinueTransaction', arguments), ); @@ -52,13 +52,13 @@ void main() { test( 'handlePaymentQueueDelegateCallbacks should call SKPaymentQueueDelegateWrapper.shouldShowPriceConsent', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); + final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + final TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); await queue.setDelegate(testDelegate); - final result = await queue.handlePaymentQueueDelegateCallbacks( - MethodCall('shouldShowPriceConsent'), - ); + final bool result = (await queue.handlePaymentQueueDelegateCallbacks( + const MethodCall('shouldShowPriceConsent'), + ))! as bool; expect(result, false); expect( @@ -72,12 +72,12 @@ void main() { test( 'handleObserverCallbacks should call SKTransactionObserverWrapper.restoreCompletedTransactionsFailed', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestTransactionObserverWrapper testObserver = + final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + final TestTransactionObserverWrapper testObserver = TestTransactionObserverWrapper(); queue.setTransactionObserver(testObserver); - final arguments = { + final Map arguments = { 'code': 100, 'domain': 'domain', 'userInfo': {'error': 'underlying_error'}, @@ -146,8 +146,8 @@ class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper { } } -class FakeIOSPlatform { - FakeIOSPlatform() { +class FakeStoreKitPlatform { + FakeStoreKitPlatform() { channel.setMockMethodCallHandler(onMethodCall); } @@ -163,6 +163,6 @@ class FakeIOSPlatform { isPaymentQueueDelegateRegistered = false; return Future.sync(() {}); } - return Future.error('method not mocked'); + return Future.error('method not mocked'); } } diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 88% rename from packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart rename to packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart index 6a33b75d9808..12fb21436ace 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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,10 +2,9 @@ // 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_ios/src/types/app_store_product_details.dart'; -import 'package:in_app_purchase_ios/src/types/app_store_purchase_details.dart'; -import 'package:in_app_purchase_ios/src/store_kit_wrappers/sk_product_wrapper.dart'; -import 'package:in_app_purchase_ios/store_kit_wrappers.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'; import 'package:test/test.dart'; import 'sk_test_stub_objects.dart'; @@ -17,7 +16,7 @@ void main() { () { final SKProductSubscriptionPeriodWrapper wrapper = SKProductSubscriptionPeriodWrapper.fromJson( - buildSubscriptionPeriodMap(dummySubscription)!); + buildSubscriptionPeriodMap(dummySubscription)); expect(wrapper, equals(dummySubscription)); }); @@ -25,7 +24,8 @@ void main() { 'SKProductSubscriptionPeriodWrapper should have properties to be default values if map is empty', () { final SKProductSubscriptionPeriodWrapper wrapper = - SKProductSubscriptionPeriodWrapper.fromJson({}); + SKProductSubscriptionPeriodWrapper.fromJson( + const {}); expect(wrapper.numberOfUnits, 0); expect(wrapper.unit, SKSubscriptionPeriodUnit.day); }); @@ -42,7 +42,7 @@ void main() { 'SKProductDiscountWrapper should have properties to be default if map is empty', () { final SKProductDiscountWrapper wrapper = - SKProductDiscountWrapper.fromJson({}); + SKProductDiscountWrapper.fromJson(const {}); expect(wrapper.price, ''); expect( wrapper.priceLocale, @@ -70,7 +70,7 @@ void main() { 'SKProductWrapper should have properties to be default if map is empty', () { final SKProductWrapper wrapper = - SKProductWrapper.fromJson({}); + SKProductWrapper.fromJson(const {}); expect(wrapper.productIdentifier, ''); expect(wrapper.localizedTitle, ''); expect(wrapper.localizedDescription, ''); @@ -84,6 +84,7 @@ void main() { expect(wrapper.subscriptionGroupIdentifier, null); expect(wrapper.price, ''); expect(wrapper.subscriptionPeriod, null); + expect(wrapper.discounts, []); }); test('toProductDetails() should return correct Product object', () { @@ -94,8 +95,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); }); @@ -126,25 +126,25 @@ void main() { group('Payment queue related object tests', () { test('Should construct correct SKPaymentWrapper from json', () { - SKPaymentWrapper payment = + final SKPaymentWrapper payment = SKPaymentWrapper.fromJson(dummyPayment.toMap()); expect(payment, equals(dummyPayment)); }); test('Should construct correct SKError from json', () { - SKError error = SKError.fromJson(buildErrorMap(dummyError)); + final SKError error = SKError.fromJson(buildErrorMap(dummyError)); expect(error, equals(dummyError)); }); test('Should construct correct SKTransactionWrapper from json', () { - SKPaymentTransactionWrapper transaction = + final SKPaymentTransactionWrapper transaction = SKPaymentTransactionWrapper.fromJson( buildTransactionMap(dummyTransaction)); expect(transaction, equals(dummyTransaction)); }); test('toPurchaseDetails() should return correct PurchaseDetail object', () { - AppStorePurchaseDetails details = + final AppStorePurchaseDetails details = AppStorePurchaseDetails.fromSKTransaction( dummyTransaction, 'receipt data'); expect(dummyTransaction.transactionIdentifier, details.purchaseID); @@ -182,7 +182,7 @@ void main() { }); test('Should generate correct map of the payment object', () { - Map map = dummyPayment.toMap(); + final Map map = dummyPayment.toMap(); expect(map['productIdentifier'], dummyPayment.productIdentifier); expect(map['applicationUsername'], dummyPayment.applicationUsername); diff --git a/packages/in_app_purchase/in_app_purchase_ios/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 similarity index 84% rename from packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart rename to packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart index 595a074f1cfe..51d851cb79b5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/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 @@ -2,16 +2,18 @@ // 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_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; -final dummyPayment = SKPaymentWrapper( +const SKPaymentWrapper dummyPayment = SKPaymentWrapper( productIdentifier: 'prod-id', applicationUsername: 'app-user-name', requestData: 'fake-data-utf8', quantity: 2, simulatesAskToBuyInSandbox: true); -final SKError dummyError = - SKError(code: 111, domain: 'dummy-domain', userInfo: {'key': 'value'}); +const SKError dummyError = SKError( + code: 111, + domain: 'dummy-domain', + userInfo: {'key': 'value'}); final SKPaymentTransactionWrapper dummyOriginalTransaction = SKPaymentTransactionWrapper( @@ -34,7 +36,7 @@ final SKPaymentTransactionWrapper dummyTransaction = ); final SKPriceLocaleWrapper dollarLocale = SKPriceLocaleWrapper( - currencySymbol: '\$', + currencySymbol: r'$', currencyCode: 'USD', countryCode: 'US', ); @@ -68,16 +70,17 @@ final SKProductWrapper dummyProductWrapper = SKProductWrapper( price: '1.0', subscriptionPeriod: dummySubscription, introductoryPrice: dummyDiscount, + discounts: [dummyDiscount], ); final SkProductResponseWrapper dummyProductResponseWrapper = SkProductResponseWrapper( - products: [dummyProductWrapper], - invalidProductIdentifiers: ['123'], + products: [dummyProductWrapper], + invalidProductIdentifiers: const ['123'], ); Map buildLocaleMap(SKPriceLocaleWrapper local) { - return { + return { 'currencySymbol': local.currencySymbol, 'currencyCode': local.currencyCode, 'countryCode': local.countryCode, @@ -89,14 +92,14 @@ Map? buildSubscriptionPeriodMap( if (sub == null) { return null; } - return { + return { 'numberOfUnits': sub.numberOfUnits, 'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit), }; } Map buildDiscountMap(SKProductDiscountWrapper discount) { - return { + return { 'price': discount.price, 'priceLocale': buildLocaleMap(discount.priceLocale), 'numberOfPeriods': discount.numberOfPeriods, @@ -108,7 +111,7 @@ Map buildDiscountMap(SKProductDiscountWrapper discount) { } Map buildProductMap(SKProductWrapper product) { - return { + return { 'productIdentifier': product.productIdentifier, 'localizedTitle': product.localizedTitle, 'localizedDescription': product.localizedDescription, @@ -118,22 +121,23 @@ Map buildProductMap(SKProductWrapper product) { 'subscriptionPeriod': buildSubscriptionPeriodMap(product.subscriptionPeriod), 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), + 'discounts': [buildDiscountMap(product.introductoryPrice!)], }; } Map buildProductResponseMap( SkProductResponseWrapper response) { - List productsMap = response.products + final List productsMap = response.products .map((SKProductWrapper product) => buildProductMap(product)) .toList(); - return { + return { 'products': productsMap, 'invalidProductIdentifiers': response.invalidProductIdentifiers }; } Map buildErrorMap(SKError error) { - return { + return { 'code': error.code, 'domain': error.domain, 'userInfo': error.userInfo, @@ -142,7 +146,7 @@ Map buildErrorMap(SKError error) { Map buildTransactionMap( SKPaymentTransactionWrapper transaction) { - Map map = { + final Map map = { 'transactionState': SKPaymentTransactionStateWrapper.values .indexOf(SKPaymentTransactionStateWrapper.purchased), 'payment': transaction.payment.toMap(), diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index 67f658b56327..83c4adb500f0 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -1,9 +1,12 @@ -# integration_test (deprecated) +# integration_test (moved) -## DEPRECATED +## MOVED -This package has been moved to the Flutter SDK. Starting with Flutter 2.0, -it should be included as: +This package has [moved to the Flutter +SDK](https://github.com/flutter/flutter/tree/master/packages/integration_test), +and the pub.dev version is deprecated. +As of Flutter 2.0, include it in your pubspec's +dev dependencies section, as follows: ``` dev_dependencies: @@ -11,255 +14,5 @@ dev_dependencies: sdk: flutter ``` -## 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 -``` +For the latest documentation, see [Integration +testing](https://flutter.dev/docs/testing/integration-tests). diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 2ebd1d1d2d7c..eadf63de9b92 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,38 @@ +## NEXT + +* 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. + +## 0.2.0+3 + +* Internal fix for unused field formal parameter. + ## 0.2.0+2 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. 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/analysis_options.yaml b/packages/ios_platform_images/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/ios_platform_images/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml 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/ios/RunnerTests/IosPlatformImagesTests.m b/packages/ios_platform_images/example/ios/RunnerTests/IosPlatformImagesTests.m index 747719d30276..c95c6ad5730d 100644 --- a/packages/ios_platform_images/example/ios/RunnerTests/IosPlatformImagesTests.m +++ b/packages/ios_platform_images/example/ios/RunnerTests/IosPlatformImagesTests.m @@ -11,7 +11,7 @@ @interface IosPlatformImagesTests : XCTestCase @implementation IosPlatformImagesTests - (void)testPlugin { - IosPlatformImagesPlugin* plugin = [[IosPlatformImagesPlugin alloc] init]; + IosPlatformImagesPlugin *plugin = [[IosPlatformImagesPlugin alloc] init]; XCTAssertNotNil(plugin); } diff --git a/packages/ios_platform_images/example/lib/main.dart b/packages/ios_platform_images/example/lib/main.dart index 1546edca8c90..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 { @@ -18,7 +21,8 @@ class _MyAppState extends State { void initState() { super.initState(); - IosPlatformImages.resolveURL("textfile").then((value) => print(value)); + IosPlatformImages.resolveURL('textfile') + .then((String? value) => print(value)); } @override @@ -31,7 +35,7 @@ class _MyAppState extends State { body: Center( // "flutter" is a resource in Assets.xcassets. child: Image( - image: IosPlatformImages.load("flutter"), + image: IosPlatformImages.load('flutter'), semanticLabel: 'Flutter logo', ), ), diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index 97241b677295..10be0d6be998 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -4,14 +4,13 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" dependencies: + cupertino_icons: ^1.0.2 flutter: sdk: flutter - cupertino_icons: ^1.0.2 - dev_dependencies: flutter_test: sdk: flutter @@ -22,7 +21,6 @@ dev_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: ../ - pedantic: ^1.10.0 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 09fa35c27a6b..f3cd4c68b65b 100644 --- a/packages/ios_platform_images/example/test/widget_test.dart +++ b/packages/ios_platform_images/example/test/widget_test.dart @@ -12,13 +12,12 @@ 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()); - // Verify that platform version is retrieved. expect( find.byWidgetPredicate( (Widget widget) => - widget is Image && (Platform.isIOS ? widget.image != null : true), + widget is Image && (!Platform.isIOS || widget.image != null), ), findsOneWidget, ); diff --git a/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m b/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m index abd331e5d0cb..5f7debc3fe07 100644 --- a/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m +++ b/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m @@ -13,16 +13,16 @@ @interface IosPlatformImagesPlugin () @implementation IosPlatformImagesPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/ios_platform_images" binaryMessenger:[registrar messenger]]; - [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { if ([@"loadImage" isEqualToString:call.method]) { - NSString* name = call.arguments; - UIImage* image = [UIImage imageNamed:name]; - NSData* data = UIImagePNGRepresentation(image); + NSString *name = call.arguments; + UIImage *image = [UIImage imageNamed:name]; + NSData *data = UIImagePNGRepresentation(image); if (data) { result(@{ @"scale" : @(image.scale), @@ -33,11 +33,11 @@ + (void)registerWithRegistrar:(NSObject*)registrar { } return; } else if ([@"resolveURL" isEqualToString:call.method]) { - NSArray* args = call.arguments; - NSString* name = args[0]; - NSString* extension = (args[1] == (id)NSNull.null) ? nil : args[1]; + NSArray *args = call.arguments; + NSString *name = args[0]; + NSString *extension = (args[1] == (id)NSNull.null) ? nil : args[1]; - NSURL* url = [[NSBundle mainBundle] URLForResource:name withExtension:extension]; + NSURL *url = [[NSBundle mainBundle] URLForResource:name withExtension:extension]; result(url.absoluteString); return; } diff --git a/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.h b/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.h index b98a423fa915..356a5f1cfe3e 100644 --- a/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.h +++ b/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.h @@ -22,6 +22,6 @@ /// /// Note: We don't yet support images from package dependencies (ex. /// `AssetImage('icons/heart.png', package: 'my_icons')`). -+ (UIImage*)flutterImageWithName:(NSString*)name; ++ (UIImage *)flutterImageWithName:(NSString *)name; @end diff --git a/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.m b/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.m index c6b8d8e91d1b..f20bbcd08c9b 100644 --- a/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.m +++ b/packages/ios_platform_images/ios/Classes/UIImage+ios_platform_images.m @@ -6,20 +6,20 @@ #import "UIImage+ios_platform_images.h" @implementation UIImage (ios_platform_images) -+ (UIImage*)flutterImageWithName:(NSString*)name { - NSString* filename = [name lastPathComponent]; - NSString* path = [name stringByDeletingLastPathComponent]; ++ (UIImage *)flutterImageWithName:(NSString *)name { + NSString *filename = [name lastPathComponent]; + NSString *path = [name stringByDeletingLastPathComponent]; for (int screenScale = [UIScreen mainScreen].scale; screenScale > 1; --screenScale) { - NSString* key = [FlutterDartProject + NSString *key = [FlutterDartProject lookupKeyForAsset:[NSString stringWithFormat:@"%@/%d.0x/%@", path, screenScale, filename]]; - UIImage* image = [UIImage imageNamed:key + UIImage *image = [UIImage imageNamed:key inBundle:[NSBundle mainBundle] compatibleWithTraitCollection:nil]; if (image) { return image; } } - NSString* key = [FlutterDartProject lookupKeyForAsset:name]; + NSString *key = [FlutterDartProject lookupKeyForAsset:name]; return [UIImage imageNamed:key inBundle:[NSBundle mainBundle] compatibleWithTraitCollection:nil]; } @end diff --git a/packages/ios_platform_images/ios/ios_platform_images.podspec b/packages/ios_platform_images/ios/ios_platform_images.podspec index ccbb9f9bda8a..3549277e9d86 100644 --- a/packages/ios_platform_images/ios/ios_platform_images.podspec +++ b/packages/ios_platform_images/ios/ios_platform_images.podspec @@ -13,7 +13,7 @@ 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/master/packages/ios_platform_images' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/ios_platform_images' } s.documentation_url = 'https://pub.dev/packages/ios_platform_images' s.source_files = 'Classes/**/*' s.dependency 'Flutter' diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index e9bc0b342239..fa40eb08fafd 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -3,46 +3,43 @@ // 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/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/painting.dart'; -import 'package:flutter/foundation.dart' - show SynchronousFuture, describeIdentity; class _FutureImageStreamCompleter extends ImageStreamCompleter { _FutureImageStreamCompleter({ required Future codec, required this.futureScale, - this.informationCollector, }) { - codec.then(_onCodecReady, onError: (dynamic error, StackTrace stack) { + codec.then(_onCodecReady, onError: (Object error, StackTrace stack) { reportError( context: ErrorDescription('resolving a single-frame image stream'), exception: error, stack: stack, - informationCollector: informationCollector, silent: true, ); }); } final Future futureScale; - final InformationCollector? informationCollector; Future _onCodecReady(ui.Codec codec) async { try { - ui.FrameInfo nextFrame = await codec.getNextFrame(); - double scale = await futureScale; + final ui.FrameInfo nextFrame = await codec.getNextFrame(); + final double scale = await futureScale; setImage(ImageInfo(image: nextFrame.image, scale: scale)); } catch (exception, stack) { reportError( context: ErrorDescription('resolving an image frame'), exception: exception, stack: stack, - informationCollector: this.informationCollector, silent: true, ); } @@ -51,6 +48,7 @@ class _FutureImageStreamCompleter extends ImageStreamCompleter { /// Performs exactly like a [MemoryImage] but instead of taking in bytes it takes /// in a future that represents bytes. +@immutable class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { /// Constructor for FutureMemoryImage. [_futureBytes] is the bytes that will /// be loaded into an image and [_futureScale] is the scale that will be applied to @@ -66,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), @@ -77,6 +78,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { Future _loadAsync( _FutureMemoryImage key, + // ignore: deprecated_member_use DecoderCallback decode, ) async { assert(key == this); @@ -87,23 +89,26 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { /// See [ImageProvider.operator==]. @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) return false; - final _FutureMemoryImage typedOther = other; - return _futureBytes == typedOther._futureBytes && - _futureScale == typedOther._futureScale; + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _FutureMemoryImage && + _futureBytes == other._futureBytes && + _futureScale == other._futureScale; } /// See [ImageProvider.hashCode]. @override - int get hashCode => hashValues(_futureBytes.hashCode, _futureScale); + int get hashCode => Object.hash(_futureBytes.hashCode, _futureScale); /// See [ImageProvider.toString]. @override - String toString() => - '$runtimeType(${describeIdentity(_futureBytes)}, scale: $_futureScale)'; + String toString() => '${objectRuntimeType(this, '_FutureMemoryImage')}' + '(${describeIdentity(_futureBytes)}, scale: $_futureScale)'; } +// ignore: avoid_classes_with_only_static_members /// Class to help loading of iOS platform images into Flutter. /// /// For example, loading an image that is in `Assets.xcassts`. @@ -118,10 +123,11 @@ class IosPlatformImages { /// /// See [https://developer.apple.com/documentation/uikit/uiimage/1624146-imagenamed?language=objc] static ImageProvider load(String name) { - Future loadInfo = _channel.invokeMapMethod('loadImage', name); - Completer bytesCompleter = Completer(); - Completer scaleCompleter = Completer(); - loadInfo.then((map) { + final Future?> loadInfo = + _channel.invokeMapMethod('loadImage', name); + final Completer bytesCompleter = Completer(); + final Completer scaleCompleter = Completer(); + loadInfo.then((Map? map) { if (map == null) { scaleCompleter.completeError( Exception("Image couldn't be found: $name"), @@ -131,8 +137,8 @@ class IosPlatformImages { ); return; } - scaleCompleter.complete(map["scale"]); - bytesCompleter.complete(map["data"]); + scaleCompleter.complete(map['scale']! as double); + bytesCompleter.complete(map['data']! as Uint8List); }); return _FutureMemoryImage(bytesCompleter.future, scaleCompleter.future); } @@ -144,6 +150,7 @@ class IosPlatformImages { /// /// See [https://developer.apple.com/documentation/foundation/nsbundle/1411540-urlforresource?language=objc] static Future resolveURL(String name, {String? extension}) { - return _channel.invokeMethod('resolveURL', [name, extension]); + return _channel + .invokeMethod('resolveURL', [name, extension]); } } diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index adc8dc08011e..c4397c551c27 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -1,12 +1,12 @@ 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/master/packages/ios_platform_images +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+2 +version: 0.2.0+9 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: diff --git a/packages/ios_platform_images/test/ios_platform_images_test.dart b/packages/ios_platform_images/test/ios_platform_images_test.dart index a896e3d835af..76b012002dfa 100644 --- a/packages/ios_platform_images/test/ios_platform_images_test.dart +++ b/packages/ios_platform_images/test/ios_platform_images_test.dart @@ -23,6 +23,6 @@ void main() { }); test('resolveURL', () async { - expect(await IosPlatformImages.resolveURL("foobar"), '42'); + expect(await IosPlatformImages.resolveURL('foobar'), '42'); }); } diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md deleted file mode 100644 index f4129f77f5d4..000000000000 --- a/packages/local_auth/CHANGELOG.md +++ /dev/null @@ -1,229 +0,0 @@ -## 1.1.8 - -* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. -* Updated Android lint settings. - -## 1.1.7 - -* Remove references to the Android V1 embedding. - -## 1.1.6 - -* Migrate maven repository from jcenter to mavenCentral. - -## 1.1.5 - -* Updated grammatical errors and inaccurate information in README. - -## 1.1.4 - -* Add debug assertion that `localizedReason` in `LocalAuthentication.authenticateWithBiometrics` must not be empty. - -## 1.1.3 - -* Fix crashes due to threading issues in iOS implementation. - -## 1.1.2 - -* Update Jetpack dependencies to latest stable versions. - -## 1.1.1 - -* Update flutter_plugin_android_lifecycle dependency to 2.0.1 to fix an R8 issue - on some versions. - -## 1.1.0 - -* Migrate to null safety. -* Allow pin, passcode, and pattern authentication with `authenticate` method. -* Fix incorrect error handling switch case fallthrough. -* Update README for Android Integration. -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)). -* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class - * `fingerprintHint` is now `biometricHint` - * `fingerprintNotRecognized`is now `biometricNotRecognized` - * `fingerprintSuccess`is now `biometricSuccess` - * `fingerprintRequiredTitle` is now `biometricRequiredTitle` - -## 0.6.3+5 - -* Update Flutter SDK constraint. - -## 0.6.3+4 - -* Update Dart SDK constraint in example. - -## 0.6.3+3 - -* Update android compileSdkVersion to 29. - -## 0.6.3+2 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.6.3+1 - -* Update package:e2e -> package:integration_test - -## 0.6.3 - -* Increase upper range of `package:platform` constraint to allow 3.X versions. - -## 0.6.2+4 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.6.2+3 - -* Post-v2 Android embedding cleanup. - -## 0.6.2+2 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.6.2+1 - -* Fix CocoaPods podspec lint warnings. - -## 0.6.2 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. -* Fix block implicitly retains 'self' warning. - -## 0.6.1+4 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.6.1+3 - -* Make the pedantic dev_dependency explicit. - -## 0.6.1+2 - -* Support v2 embedding. - -## 0.6.1+1 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.6.1 - -* Added ability to stop authentication (For Android). - -## 0.6.0+3 - -* Remove AndroidX warnings. - -## 0.6.0+2 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.6.0+1 - -* Update the `intl` constraint to ">=0.15.1 <0.17.0" (0.16.0 isn't really a breaking change). - -## 0.6.0 - -* Define a new parameter for signaling that the transaction is sensitive. -* Up the biometric version to beta01. -* Handle no device credential error. - -## 0.5.3 - -* Add face id detection as well by not relying on FingerprintCompat. - -## 0.5.2+4 - -* Update README to fix syntax error. - -## 0.5.2+3 - -* Update documentation to clarify the need for FragmentActivity. - -## 0.5.2+2 - -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.5.2+1 -* Use post instead of postDelayed to show the dialog onResume. - -## 0.5.2 -* Executor thread needs to be UI thread. - -## 0.5.1 -* Fix crash on Android versions earlier than 28. -* [`authenticateWithBiometrics`](https://pub.dev/documentation/local_auth/latest/local_auth/LocalAuthentication/authenticateWithBiometrics.html) will not return result unless Biometric Dialog is closed. -* Added two more error codes `LockedOut` and `PermanentlyLockedOut`. - -## 0.5.0 - * **Breaking change**. Update the Android API to use androidx Biometric package. This gives - the prompt the updated Material look. However, it also requires the activity to be a - FragmentActivity. Users can switch to FlutterFragmentActivity in their main app to migrate. - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.1 -* Fix crash on Android versions earlier than 24. - -## 0.3.0 - -* **Breaking change**. Add canCheckBiometrics and getAvailableBiometrics which leads to a new API. - -## 0.2.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.2 - -* Fixed Dart 2 type error. - -## 0.1.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.3 - -* Add FLT prefix to iOS types - -## 0.0.2+1 - -* Update messaging to support Face ID. - -## 0.0.2 - -* Support stickyAuth mode. - -## 0.0.1 - -* Initial release of local authentication plugin. diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md deleted file mode 100644 index 84470c646e6b..000000000000 --- a/packages/local_auth/README.md +++ /dev/null @@ -1,223 +0,0 @@ -# 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). - -## Usage in Dart - -Import the relevant file: - -```dart -import 'package:local_auth/local_auth.dart'; -``` - -To check whether there is local authentication available on this device or not, call canCheckBiometrics: - -```dart -bool canCheckBiometrics = - await localAuth.canCheckBiometrics; -``` - -Currently the following biometric types are implemented: - -- BiometricType.face -- BiometricType.fingerprint - -To get a list of enrolled biometrics, call getAvailableBiometrics: - -```dart -List availableBiometrics = - await auth.getAvailableBiometrics(); - -if (Platform.isIOS) { - if (availableBiometrics.contains(BiometricType.face)) { - // Face ID. - } else if (availableBiometrics.contains(BiometricType.fingerprint)) { - // Touch ID. - } -} -``` - -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'. - -Use the exported APIs to trigger local authentication with default dialogs: - -The `authenticate()` method uses biometric authentication, but also allows -users to use pin, pattern, or passcode. - -```dart -var localAuth = LocalAuthentication(); -bool didAuthenticate = - await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance'); -``` - -To authenticate using biometric authentication only, set `biometricOnly` to `true`. - -```dart -var localAuth = LocalAuthentication(); -bool didAuthenticate = - await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance', - biometricOnly: true); -``` - -If you don't want to use the default dialogs, call this API with -'useErrorDialogs = false'. In this case, it will throw the error message back -and you need to handle them in your dart code: - -```dart -bool didAuthenticate = - await localAuth.authenticate( - localizedReason: 'Please authenticate to show account balance', - useErrorDialogs: false); -``` - -You can use our default dialog messages, or you can use your own messages by -passing in IOSAuthMessages and AndroidAuthMessages: - -```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); - -``` - -If needed, you can manually stop authentication for android: - -```dart - -void _cancelAuthentication() { - localAuth.stopAuthentication(); -} - -``` - -### 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: - -```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. - } -} -``` - -## iOS Integration - -Note that this plugin works with both Touch ID and Face ID. However, to use the latter, -you need to also add: - -```xml -NSFaceIDUsageDescription -Why is my app authenticating using face id? -``` - -to your Info.plist file. Failure to do so results in a dialog that tells the user your -app has not been updated to use 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")); - } -} -``` - -OR - -Update your MainActivity.kt: - -```kotlin -import io.flutter.embedding.android.FlutterFragmentActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterFragmentActivity() { - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine) - } -} -``` - -Update your project's `AndroidManifest.xml` file to include the -`USE_FINGERPRINT` permissions: - -```xml - - - -``` - -On Android, you can check only for existence of fingerprint hardware prior -to API 29 (Android Q). Therefore, if you would like to support other biometrics -types (such as face scanning) and you want to support SDKs lower than Q, -_do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`. -This will return an error if there was no hardware available. - -## Sticky Auth - -You can set the `stickyAuth` option on the plugin to true so that plugin does not -return failure if the app is put to background by the system. This might happen -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/analysis_options.yaml b/packages/local_auth/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/local_auth/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle deleted file mode 100644 index dc282e78ced0..000000000000 --- a/packages/local_auth/android/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -group 'io.flutter.plugins.localauth' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - baseline file("lint-baseline.xml") - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - api "androidx.core:core:1.3.2" - 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' - 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/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java deleted file mode 100644 index 522185fc9dd3..000000000000 --- a/packages/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ /dev/null @@ -1,22 +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.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -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); - } -} diff --git a/packages/local_auth/example/README.md b/packages/local_auth/example/README.md deleted file mode 100644 index a4a6091c9ba6..000000000000 --- a/packages/local_auth/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# 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/example/android.iml b/packages/local_auth/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/local_auth/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/local_auth/example/android/app/build.gradle b/packages/local_auth/example/android/app/build.gradle deleted file mode 100644 index 34b3fe2d69e3..000000000000 --- a/packages/local_auth/example/android/app/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -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 29 - - 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.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/local_auth/example/ios/Podfile b/packages/local_auth/example/ios/Podfile deleted file mode 100644 index ef20d8e3c010..000000000000 --- a/packages/local_auth/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# 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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 3de4b94f9d5c..000000000000 --- a/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,630 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - 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 */; }; - 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 */; }; - B726772E092FC537C9618264 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */; }; -/* 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 = ""; }; - 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.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 = ""; }; - 99302E79EC77497F2F274D12 /* 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 = ""; }; - 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; - FEA527BB0A821430FEAA1566 /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 3398D2CA26163948005A052F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B726772E092FC537C9618264 /* 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 */, - 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - F8CC53B854B121315C7319D2 /* Pods */ = { - isa = PBXGroup; - children = ( - EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */, - 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */, - 99302E79EC77497F2F274D12 /* Pods-RunnerTests.debug.xcconfig */, - FEA527BB0A821430FEAA1566 /* 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 = ( - B5AF6C7A6759E6F38749E537 /* [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 = 1100; - 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; - }; - 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; - 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 = 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 = { - 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/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 58a5d07a15c8..000000000000 --- a/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/local_auth/example/ios/Runner/main.m b/packages/local_auth/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/local_auth/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#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/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m deleted file mode 100644 index 97e78e2f624b..000000000000 --- a/packages/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ /dev/null @@ -1,189 +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 LocalAuthentication; -@import XCTest; - -#import - -#if __has_include() -#import -#else -@import local_auth; -#endif - -// Private API needed for tests. -@interface FLTLocalAuthPlugin (Test) -- (void)setAuthContextOverrides:(NSArray*)authContexts; -@end - -// Set a long timeout to avoid flake due to slow CI. -static const NSTimeInterval kTimeout = 30.0; - -@interface FLTLocalAuthPluginTests : XCTestCase -@end - -@implementation FLTLocalAuthPluginTests - -- (void)setUp { - self.continueAfterFailure = NO; -} - -- (void)testSuccessfullAuthWithBiometrics { - FLTLocalAuthPlugin* plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSString* reason = @"a reason"; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation*) = ^(NSInvocation* invocation) { - void (^reply)(BOOL, NSError*); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - - 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)testSuccessfullAuthWithoutBiometrics { - FLTLocalAuthPlugin* plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString* reason = @"a reason"; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation*) = ^(NSInvocation* invocation) { - void (^reply)(BOOL, NSError*); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - - 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)testFailedAuthWithBiometrics { - FLTLocalAuthPlugin* plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSString* reason = @"a reason"; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation*) = ^(NSInvocation* invocation) { - void (^reply)(BOOL, NSError*); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(YES), - @"localizedReason" : reason, - }]; - - 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)testFailedAuthWithoutBiometrics { - FLTLocalAuthPlugin* plugin = [[FLTLocalAuthPlugin alloc] init]; - id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; - NSString* reason = @"a reason"; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation*) = ^(NSInvocation* invocation) { - void (^reply)(BOOL, NSError*); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); - - FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" - arguments:@{ - @"biometricOnly" : @(NO), - @"localizedReason" : reason, - }]; - - 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]; -} - -@end diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart deleted file mode 100644 index b6b6f3278423..000000000000 --- a/packages/local_auth/example/lib/main.dart +++ /dev/null @@ -1,219 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:local_auth/local_auth.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - final LocalAuthentication auth = LocalAuthentication(); - _SupportState _supportState = _SupportState.unknown; - bool? _canCheckBiometrics; - List? _availableBiometrics; - String _authorized = 'Not Authorized'; - bool _isAuthenticating = false; - - @override - void initState() { - super.initState(); - auth.isDeviceSupported().then( - (isSupported) => setState(() => _supportState = isSupported - ? _SupportState.supported - : _SupportState.unsupported), - ); - } - - Future _checkBiometrics() async { - late bool canCheckBiometrics; - try { - canCheckBiometrics = await auth.canCheckBiometrics; - } on PlatformException catch (e) { - canCheckBiometrics = false; - print(e); - } - if (!mounted) return; - - setState(() { - _canCheckBiometrics = canCheckBiometrics; - }); - } - - Future _getAvailableBiometrics() async { - late List availableBiometrics; - try { - availableBiometrics = await auth.getAvailableBiometrics(); - } on PlatformException catch (e) { - availableBiometrics = []; - print(e); - } - if (!mounted) return; - - setState(() { - _availableBiometrics = availableBiometrics; - }); - } - - Future _authenticate() async { - bool authenticated = false; - try { - setState(() { - _isAuthenticating = true; - _authorized = 'Authenticating'; - }); - authenticated = await auth.authenticate( - localizedReason: 'Let OS determine authentication method', - useErrorDialogs: true, - 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 auth.authenticate( - localizedReason: - 'Scan your fingerprint (or face or whatever) to authenticate', - useErrorDialogs: true, - stickyAuth: true, - biometricOnly: true); - setState(() { - _isAuthenticating = false; - _authorized = 'Authenticating'; - }); - } on PlatformException catch (e) { - print(e); - setState(() { - _isAuthenticating = false; - _authorized = "Error - ${e.message}"; - }); - return; - } - if (!mounted) return; - - final String message = authenticated ? 'Authorized' : 'Not Authorized'; - setState(() { - _authorized = message; - }); - } - - void _cancelAuthentication() async { - await auth.stopAuthentication(); - setState(() => _isAuthenticating = false); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: ListView( - padding: const EdgeInsets.only(top: 30), - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_supportState == _SupportState.unknown) - CircularProgressIndicator() - else if (_supportState == _SupportState.supported) - Text("This device is supported") - else - Text("This device is not supported"), - Divider(height: 100), - Text('Can check biometrics: $_canCheckBiometrics\n'), - ElevatedButton( - child: const Text('Check biometrics'), - onPressed: _checkBiometrics, - ), - Divider(height: 100), - Text('Available biometrics: $_availableBiometrics\n'), - ElevatedButton( - child: const Text('Get available biometrics'), - onPressed: _getAvailableBiometrics, - ), - Divider(height: 100), - Text('Current State: $_authorized\n'), - (_isAuthenticating) - ? ElevatedButton( - onPressed: _cancelAuthentication, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Cancel Authentication"), - Icon(Icons.cancel), - ], - ), - ) - : Column( - children: [ - ElevatedButton( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Authenticate'), - Icon(Icons.perm_device_information), - ], - ), - onPressed: _authenticate, - ), - ElevatedButton( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_isAuthenticating - ? 'Cancel' - : 'Authenticate: biometrics only'), - Icon(Icons.fingerprint), - ], - ), - onPressed: _authenticateWithBiometrics, - ), - ], - ), - ], - ), - ], - ), - ), - ); - } -} - -enum _SupportState { - unknown, - supported, - unsupported, -} diff --git a/packages/local_auth/example/local_auth_example.iml b/packages/local_auth/example/local_auth_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/local_auth/example/local_auth_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/local_auth/example/local_auth_example_android.iml b/packages/local_auth/example/local_auth_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/local_auth/example/local_auth_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml deleted file mode 100644 index 3aa8fd848057..000000000000 --- a/packages/local_auth/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: local_auth_example -description: Demonstrates how to use the local_auth plugin. -publish_to: none - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - local_auth: - # When depending on this package from a real application you should use: - # local_auth: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - integration_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m deleted file mode 100644 index c2dc9db25fc8..000000000000 --- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ /dev/null @@ -1,229 +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 "FLTLocalAuthPlugin.h" - -@interface FLTLocalAuthPlugin () -@property(nonatomic, copy, nullable) NSDictionary *lastCallArgs; -@property(nonatomic, nullable) FlutterResult lastResult; -// For unit tests to inject dummy LAContext instances that will be used when a new context would -// normally be created. Each call to createAuthContext will remove the current first element from -// the array. -- (void)setAuthContextOverrides:(NSArray *)authContexts; -@end - -@implementation FLTLocalAuthPlugin { - NSMutableArray *_authContextOverrides; -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth" - binaryMessenger:[registrar messenger]]; - FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; - [registrar addApplicationDelegate:instance]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticate" isEqualToString:call.method]) { - bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; - if (isBiometricOnly) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; - } else { - [self authenticate:call.arguments withFlutterResult:result]; - } - } else if ([@"getAvailableBiometrics" isEqualToString:call.method]) { - [self getAvailableBiometrics:result]; - } else if ([@"isDeviceSupported" isEqualToString:call.method]) { - result(@YES); - } else { - result(FlutterMethodNotImplemented); - } -} - -#pragma mark Private Methods - -- (void)setAuthContextOverrides:(NSArray *)authContexts { - _authContextOverrides = [authContexts mutableCopy]; -} - -- (LAContext *)createAuthContext { - if ([_authContextOverrides count] > 0) { - LAContext *context = [_authContextOverrides firstObject]; - [_authContextOverrides removeObjectAtIndex:0]; - return context; - } - return [[LAContext alloc] init]; -} - -- (void)alertMessage:(NSString *)message - firstButton:(NSString *)firstButton - flutterResult:(FlutterResult)result - additionalButton:(NSString *)secondButton { - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:@"" - message:message - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - result(@NO); - }]; - - [alert addAction:defaultAction]; - if (secondButton != nil) { - UIAlertAction *additionalAction = [UIAlertAction - actionWithTitle:secondButton - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - if (UIApplicationOpenSettingsURLString != NULL) { - NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; - [[UIApplication sharedApplication] openURL:url]; - result(@NO); - } - }]; - [alert addAction:additionalAction]; - } - [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert - animated:YES - completion:nil]; -} - -- (void)getAvailableBiometrics:(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 (context.biometryType == LABiometryTypeFaceID) { - [biometrics addObject:@"face"]; - } else if (context.biometryType == LABiometryTypeTouchID) { - [biometrics addObject:@"fingerprint"]; - } - } else { - [biometrics addObject:@"fingerprint"]; - } - } - } else if (authError.code == LAErrorTouchIDNotEnrolled) { - [biometrics addObject:@"undefined"]; - } - result(biometrics); -} - -- (void)authenticateWithBiometrics:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - self.lastCallArgs = nil; - self.lastResult = nil; - context.localizedFallbackTitle = @""; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - error:&authError]) { - [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } -} - -- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; - NSError *authError = nil; - _lastCallArgs = nil; - _lastResult = nil; - context.localizedFallbackTitle = @""; - - if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { - [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication - localizedReason:arguments[@"localizedReason"] - reply:^(BOOL success, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self handleAuthReplyWithSuccess:success - error:error - flutterArguments:arguments - flutterResult:result]; - }); - }]; - } else { - [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; - } -} - -- (void)handleAuthReplyWithSuccess:(BOOL)success - error:(NSError *)error - flutterArguments:(NSDictionary *)arguments - flutterResult:(FlutterResult)result { - NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); - if (success) { - result(@YES); - } else { - switch (error.code) { - case LAErrorPasscodeNotSet: - case LAErrorTouchIDNotAvailable: - case LAErrorTouchIDNotEnrolled: - case LAErrorTouchIDLockout: - [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; - return; - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self->_lastCallArgs = arguments; - self->_lastResult = result; - return; - } - } - result(@NO); - } -} - -- (void)handleErrors:(NSError *)authError - flutterArguments:(NSDictionary *)arguments - withFlutterResult:(FlutterResult)result { - NSString *errorCode = @"NotAvailable"; - switch (authError.code) { - case LAErrorPasscodeNotSet: - case LAErrorTouchIDNotEnrolled: - if ([arguments[@"useErrorDialogs"] boolValue]) { - [self alertMessage:arguments[@"goToSettingDescriptionIOS"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:arguments[@"goToSetting"]]; - return; - } - errorCode = authError.code == LAErrorPasscodeNotSet ? @"PasscodeNotSet" : @"NotEnrolled"; - break; - case LAErrorTouchIDLockout: - [self alertMessage:arguments[@"lockOut"] - firstButton:arguments[@"okButton"] - flutterResult:result - additionalButton:nil]; - return; - } - result([FlutterError errorWithCode:errorCode - message:authError.localizedDescription - details:authError.domain]); -} - -#pragma mark - AppDelegate - -- (void)applicationDidBecomeActive:(UIApplication *)application { - if (self.lastCallArgs != nil && self.lastResult != nil) { - [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; - } -} - -@end diff --git a/packages/local_auth/ios/local_auth.podspec b/packages/local_auth/ios/local_auth.podspec deleted file mode 100644 index 917c4bf2a0eb..000000000000 --- a/packages/local_auth/ios/local_auth.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'local_auth' - s.version = '0.0.1' - s.summary = 'Flutter Local Auth' - s.description = <<-DESC -This Flutter plugin provides means to perform local, on-device authentication of the user. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/local_auth' } - s.documentation_url = 'https://pub.dev/packages/local_auth' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart deleted file mode 100644 index 537340b79d4e..000000000000 --- a/packages/local_auth/lib/auth_strings.dart +++ /dev/null @@ -1,157 +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, - }); - - final String? lockOut; - final String? goToSettingsButton; - final String? goToSettingsDescription; - final String? cancelButton; - - Map get args { - return { - 'lockOut': lockOut ?? iOSLockOut, - 'goToSetting': goToSettingsButton ?? goToSettings, - 'goToSettingDescriptionIOS': - goToSettingsDescription ?? iOSGoToSettingsDescription, - 'okButton': cancelButton ?? iOSOkButton, - }; - } -} - -// 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/lib/error_codes.dart b/packages/local_auth/lib/error_codes.dart deleted file mode 100644 index bcf15b7b2154..000000000000 --- a/packages/local_auth/lib/error_codes.dart +++ /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. - -// Exception codes for `PlatformException` returned by -// `authenticate`. - -/// Indicates that the user has not yet configured a passcode (iOS) or -/// PIN/pattern/password (Android) on the device. -const String passcodeNotSet = 'PasscodeNotSet'; - -/// Indicates the user has not enrolled any fingerprints on the device. -const String notEnrolled = 'NotEnrolled'; - -/// Indicates the device does not have a Touch ID/fingerprint scanner. -const String notAvailable = 'NotAvailable'; - -/// Indicates the device operating system is not iOS or Android. -const String otherOperatingSystem = 'OtherOperatingSystem'; - -/// Indicates the API lock out due to too many attempts. -const String lockedOut = 'LockedOut'; - -/// Indicates the API being disabled due to too many lock outs. -/// Strong authentication like PIN/Pattern/Password is required to unlock. -const String permanentlyLockedOut = 'PermanentlyLockedOut'; diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart deleted file mode 100644 index 0b75a83d4029..000000000000 --- a/packages/local_auth/lib/local_auth.dart +++ /dev/null @@ -1,182 +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 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.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 = []; - result.forEach((String value) { - 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; - } -} diff --git a/packages/local_auth/local_auth/AUTHORS b/packages/local_auth/local_auth/AUTHORS new file mode 100644 index 000000000000..d5694690c247 --- /dev/null +++ b/packages/local_auth/local_auth/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/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md new file mode 100644 index 000000000000..1aae73d7393e --- /dev/null +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -0,0 +1,298 @@ +## 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. + +## 1.1.10 + +* Removes dependency on `meta`. + +## 1.1.9 + +* Updates code for analysis option changes. +* Updates Android compileSdkVersion to 31. + +## 1.1.8 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. +* Updated Android lint settings. + +## 1.1.7 + +* Remove references to the Android V1 embedding. + +## 1.1.6 + +* Migrate maven repository from jcenter to mavenCentral. + +## 1.1.5 + +* Updated grammatical errors and inaccurate information in README. + +## 1.1.4 + +* Add debug assertion that `localizedReason` in `LocalAuthentication.authenticateWithBiometrics` must not be empty. + +## 1.1.3 + +* Fix crashes due to threading issues in iOS implementation. + +## 1.1.2 + +* Update Jetpack dependencies to latest stable versions. + +## 1.1.1 + +* Update flutter_plugin_android_lifecycle dependency to 2.0.1 to fix an R8 issue + on some versions. + +## 1.1.0 + +* Migrate to null safety. +* Allow pin, passcode, and pattern authentication with `authenticate` method. +* Fix incorrect error handling switch case fallthrough. +* Update README for Android Integration. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)). +* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class + * `fingerprintHint` is now `biometricHint` + * `fingerprintNotRecognized`is now `biometricNotRecognized` + * `fingerprintSuccess`is now `biometricSuccess` + * `fingerprintRequiredTitle` is now `biometricRequiredTitle` + +## 0.6.3+5 + +* Update Flutter SDK constraint. + +## 0.6.3+4 + +* Update Dart SDK constraint in example. + +## 0.6.3+3 + +* Update android compileSdkVersion to 29. + +## 0.6.3+2 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.6.3+1 + +* Update package:e2e -> package:integration_test + +## 0.6.3 + +* Increase upper range of `package:platform` constraint to allow 3.X versions. + +## 0.6.2+4 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.6.2+3 + +* Post-v2 Android embedding cleanup. + +## 0.6.2+2 + +* Update lower bound of dart dependency to 2.1.0. + +## 0.6.2+1 + +* Fix CocoaPods podspec lint warnings. + +## 0.6.2 + +* Remove Android dependencies fallback. +* Require Flutter SDK 1.12.13+hotfix.5 or greater. +* Fix block implicitly retains 'self' warning. + +## 0.6.1+4 + +* Replace deprecated `getFlutterEngine` call on Android. + +## 0.6.1+3 + +* Make the pedantic dev_dependency explicit. + +## 0.6.1+2 + +* Support v2 embedding. + +## 0.6.1+1 + +* Remove the deprecated `author:` field from pubspec.yaml +* Migrate the plugin to the pubspec platforms manifest. +* Require Flutter SDK 1.10.0 or greater. + +## 0.6.1 + +* Added ability to stop authentication (For Android). + +## 0.6.0+3 + +* Remove AndroidX warnings. + +## 0.6.0+2 + +* Update and migrate iOS example project. +* Define clang module for iOS. + +## 0.6.0+1 + +* Update the `intl` constraint to ">=0.15.1 <0.17.0" (0.16.0 isn't really a breaking change). + +## 0.6.0 + +* Define a new parameter for signaling that the transaction is sensitive. +* Up the biometric version to beta01. +* Handle no device credential error. + +## 0.5.3 + +* Add face id detection as well by not relying on FingerprintCompat. + +## 0.5.2+4 + +* Update README to fix syntax error. + +## 0.5.2+3 + +* Update documentation to clarify the need for FragmentActivity. + +## 0.5.2+2 + +* Add missing template type parameter to `invokeMethod` calls. +* Bump minimum Flutter version to 1.5.0. +* Replace invokeMethod with invokeMapMethod wherever necessary. + +## 0.5.2+1 +* Use post instead of postDelayed to show the dialog onResume. + +## 0.5.2 +* Executor thread needs to be UI thread. + +## 0.5.1 +* Fix crash on Android versions earlier than 28. +* [`authenticateWithBiometrics`](https://pub.dev/documentation/local_auth/latest/local_auth/LocalAuthentication/authenticateWithBiometrics.html) will not return result unless Biometric Dialog is closed. +* Added two more error codes `LockedOut` and `PermanentlyLockedOut`. + +## 0.5.0 + * **Breaking change**. Update the Android API to use androidx Biometric package. This gives + the prompt the updated Material look. However, it also requires the activity to be a + FragmentActivity. Users can switch to FlutterFragmentActivity in their main app to migrate. + +## 0.4.0+1 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.4.0 + +* **Breaking change**. Migrate from the deprecated original Android Support + Library to AndroidX. This shouldn't result in any functional changes, but it + requires any Android apps using this plugin to [also + migrate](https://developer.android.com/jetpack/androidx/migrate) if they're + using the original support library. + +## 0.3.1 +* Fix crash on Android versions earlier than 24. + +## 0.3.0 + +* **Breaking change**. Add canCheckBiometrics and getAvailableBiometrics which leads to a new API. + +## 0.2.1 + +* Updated Gradle tooling to match Android Studio 3.1.2. + +## 0.2.0 + +* **Breaking change**. Set SDK constraints to match the Flutter beta release. + +## 0.1.2 + +* Fixed Dart 2 type error. + +## 0.1.1 + +* Simplified and upgraded Android project template to Android SDK 27. +* Updated package description. + +## 0.1.0 + +* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin + 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in + order to use this version of the plugin. Instructions can be found + [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). + +## 0.0.3 + +* Add FLT prefix to iOS types + +## 0.0.2+1 + +* Update messaging to support Face ID. + +## 0.0.2 + +* Support stickyAuth mode. + +## 0.0.1 + +* Initial release of local authentication plugin. diff --git a/packages/device_info/device_info_platform_interface/LICENSE b/packages/local_auth/local_auth/LICENSE similarity index 100% rename from packages/device_info/device_info_platform_interface/LICENSE rename to packages/local_auth/local_auth/LICENSE diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md new file mode 100644 index 000000000000..3077f8e60a1f --- /dev/null +++ b/packages/local_auth/local_auth/README.md @@ -0,0 +1,262 @@ +# local_auth + + + +This Flutter plugin provides means to perform local, on-device authentication of +the user. + +On supported devices, this includes authentication with biometrics such as +fingerprint or facial recognition. + +| | Android | iOS | Windows | +|-------------|-----------|------|-------------| +| **Support** | SDK 16+\* | 9.0+ | Windows 10+ | + +## Usage + +### Device Capabilities + +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 +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()`. + +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 +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! +} +``` + +### Options + +The `authenticate()` method uses biometric authentication when possible, but +also allows fallback to pin, pattern, or passcode. + + +```dart +try { + final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance'); + // ··· +} on PlatformException { + // ... +} +``` + +To require biometric authentication, pass `AuthenticationOptions` with +`biometricOnly` set to `true`. + + +```dart +final bool didAuthenticate = await auth.authenticate( + localizedReason: 'Please authenticate to show account balance', + options: const AuthenticationOptions(biometricOnly: true)); +``` + +*Note*: `biometricOnly` is not supported on Windows since the Windows implementation's underlying API (Windows Hello) doesn't support selecting the authentication method. + +#### Dialogs + +The plugin provides default dialogs for the following cases: + +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 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 { + // ... + } + } +``` + +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 + +`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; +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 + +Note that this plugin works with both Touch ID and Face ID. However, to use the latter, +you need to also add: + +```xml +NSFaceIDUsageDescription +Why is my app authenticating using face id? +``` + +to your Info.plist file. Failure to do so results in a dialog that tells the user your +app has not been updated to use Face ID. + +## Android Integration + +\* The plugin will build and run on SDK 16+, but `isDeviceSupported()` will +always return false before SDK 23 (Android 6.0). + +### Activity Changes + +Note that `local_auth` requires the use of a `FragmentActivity` instead of an +`Activity`. To update your application: + +* 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`: + + ```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: + +```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, +_do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`. +This will return an error if there was no hardware available. + +## Sticky Auth + +You can set the `stickyAuth` option on the plugin to true so that plugin does not +return failure if the app is put to background by the system. This might happen +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. diff --git a/packages/local_auth/local_auth/example/README.md b/packages/local_auth/local_auth/example/README.md new file mode 100644 index 000000000000..bd004a77d86b --- /dev/null +++ b/packages/local_auth/local_auth/example/README.md @@ -0,0 +1,3 @@ +# local_auth_example + +Demonstrates how to use the local_auth plugin. diff --git a/packages/local_auth/local_auth/example/android/app/build.gradle b/packages/local_auth/local_auth/example/android/app/build.gradle new file mode 100644 index 000000000000..3c6eca7ce8a7 --- /dev/null +++ b/packages/local_auth/local_auth/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/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/local_auth/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/local_auth/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java b/packages/local_auth/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java similarity index 100% rename from packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java rename to packages/local_auth/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java diff --git a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/local_auth/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/local_auth/example/android/app/src/main/AndroidManifest.xml rename to packages/local_auth/local_auth/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/local_auth/local_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/local_auth/example/android/build.gradle b/packages/local_auth/local_auth/example/android/build.gradle similarity index 100% rename from packages/local_auth/example/android/build.gradle rename to packages/local_auth/local_auth/example/android/build.gradle diff --git a/packages/local_auth/example/android/gradle.properties b/packages/local_auth/local_auth/example/android/gradle.properties similarity index 100% rename from packages/local_auth/example/android/gradle.properties rename to packages/local_auth/local_auth/example/android/gradle.properties diff --git a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/device_info/device_info/example/android/settings.gradle b/packages/local_auth/local_auth/example/android/settings.gradle similarity index 100% rename from packages/device_info/device_info/example/android/settings.gradle rename to packages/local_auth/local_auth/example/android/settings.gradle diff --git a/packages/android_alarm_manager/example/android/settings_aar.gradle b/packages/local_auth/local_auth/example/android/settings_aar.gradle similarity index 100% rename from packages/android_alarm_manager/example/android/settings_aar.gradle rename to packages/local_auth/local_auth/example/android/settings_aar.gradle diff --git a/packages/local_auth/local_auth/example/build.excerpt.yaml b/packages/local_auth/local_auth/example/build.excerpt.yaml new file mode 100644 index 000000000000..e317efa11cb3 --- /dev/null +++ b/packages/local_auth/local_auth/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/local_auth/example/integration_test/local_auth_test.dart b/packages/local_auth/local_auth/example/integration_test/local_auth_test.dart similarity index 100% rename from packages/local_auth/example/integration_test/local_auth_test.dart rename to packages/local_auth/local_auth/example/integration_test/local_auth_test.dart diff --git a/packages/local_auth/local_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/local_auth/local_auth/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/local_auth/local_auth/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/connectivity/connectivity/example/ios/Flutter/Debug.xcconfig b/packages/local_auth/local_auth/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/connectivity/connectivity/example/ios/Flutter/Debug.xcconfig rename to packages/local_auth/local_auth/example/ios/Flutter/Debug.xcconfig diff --git a/packages/connectivity/connectivity/example/ios/Flutter/Release.xcconfig b/packages/local_auth/local_auth/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/connectivity/connectivity/example/ios/Flutter/Release.xcconfig rename to packages/local_auth/local_auth/example/ios/Flutter/Release.xcconfig diff --git a/packages/battery/battery/example/ios/Podfile b/packages/local_auth/local_auth/example/ios/Podfile similarity index 100% rename from packages/battery/battery/example/ios/Podfile rename to packages/local_auth/local_auth/example/ios/Podfile diff --git a/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..b40fbca4cf66 --- /dev/null +++ b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,471 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + 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 */; }; + 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 */; }; +/* 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 = ""; }; + 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; }; + 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 = ""; }; + 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.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 = ""; }; + 99302E79EC77497F2F274D12 /* 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 = ""; }; + 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + FEA527BB0A821430FEAA1566 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */, + ); + 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 */, + F8CC53B854B121315C7319D2 /* Pods */, + E2D5FA899A019BD3E0DB0917 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E2D5FA899A019BD3E0DB0917 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3398D2DF26164A03005A052F /* liblocal_auth.a */, + 3398D2DC261649CD005A052F /* liblocal_auth.a */, + 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */, + 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F8CC53B854B121315C7319D2 /* Pods */ = { + isa = PBXGroup; + children = ( + EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */, + 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */, + 99302E79EC77497F2F274D12 /* Pods-RunnerTests.debug.xcconfig */, + FEA527BB0A821430FEAA1566 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 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 = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" 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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + 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 */ + 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/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/local_auth/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 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 new file mode 100644 index 000000000000..b2af55dd6d37 --- /dev/null +++ b/packages/local_auth/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/local_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/local_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/local_auth/local_auth/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/local_auth/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/local_auth/local_auth/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/local_auth/example/ios/Runner/AppDelegate.h b/packages/local_auth/local_auth/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/local_auth/example/ios/Runner/AppDelegate.h rename to packages/local_auth/local_auth/example/ios/Runner/AppDelegate.h diff --git a/packages/local_auth/example/ios/Runner/AppDelegate.m b/packages/local_auth/local_auth/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/local_auth/example/ios/Runner/AppDelegate.m rename to packages/local_auth/local_auth/example/ios/Runner/AppDelegate.m diff --git a/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/local_auth/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/device_info/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/local_auth/local_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/device_info/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/local_auth/local_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/local_auth/example/ios/Runner/Base.lproj/Main.storyboard b/packages/local_auth/local_auth/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/local_auth/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/local_auth/local_auth/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/local_auth/example/ios/Runner/Info.plist b/packages/local_auth/local_auth/example/ios/Runner/Info.plist similarity index 100% rename from packages/local_auth/example/ios/Runner/Info.plist rename to packages/local_auth/local_auth/example/ios/Runner/Info.plist diff --git a/packages/local_auth/local_auth/example/ios/Runner/main.m b/packages/local_auth/local_auth/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/local_auth/local_auth/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/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart new file mode 100644 index 000000000000..cc687f562402 --- /dev/null +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth/local_auth.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 { + final LocalAuthentication auth = LocalAuthentication(); + _SupportState _supportState = _SupportState.unknown; + bool? _canCheckBiometrics; + List? _availableBiometrics; + String _authorized = 'Not Authorized'; + bool _isAuthenticating = false; + + @override + void initState() { + super.initState(); + auth.isDeviceSupported().then( + (bool isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + + Future _checkBiometrics() async { + late bool canCheckBiometrics; + try { + canCheckBiometrics = await auth.canCheckBiometrics; + } on PlatformException catch (e) { + canCheckBiometrics = false; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _canCheckBiometrics = canCheckBiometrics; + }); + } + + Future _getAvailableBiometrics() async { + late List availableBiometrics; + try { + availableBiometrics = await auth.getAvailableBiometrics(); + } on PlatformException catch (e) { + availableBiometrics = []; + print(e); + } + if (!mounted) { + return; + } + + setState(() { + _availableBiometrics = availableBiometrics; + }); + } + + Future _authenticate() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await auth.authenticate( + localizedReason: 'Let OS determine authentication method', + options: const AuthenticationOptions( + useErrorDialogs: true, + 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 auth.authenticate( + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + options: const AuthenticationOptions( + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true, + ), + ); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + final String message = authenticated ? 'Authorized' : 'Not Authorized'; + setState(() { + _authorized = message; + }); + } + + Future _cancelAuthentication() async { + await auth.stopAuthentication(); + setState(() => _isAuthenticating = false); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: 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('Can check biometrics: $_canCheckBiometrics\n'), + ElevatedButton( + onPressed: _checkBiometrics, + child: const Text('Check biometrics'), + ), + const Divider(height: 100), + Text('Available biometrics: $_availableBiometrics\n'), + ElevatedButton( + onPressed: _getAvailableBiometrics, + child: const Text('Get available 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/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 new file mode 100644 index 000000000000..305005b34364 --- /dev/null +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: local_auth_example +description: Demonstrates how to use the local_auth plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + local_auth: + # When depending on this package from a real application you should use: + # local_auth: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + build_runner: ^2.1.10 + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/local_auth/local_auth/example/test_driver/integration_test.dart b/packages/local_auth/local_auth/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/local_auth/local_auth/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/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/error_codes.dart b/packages/local_auth/local_auth/lib/error_codes.dart new file mode 100644 index 000000000000..8959bf297700 --- /dev/null +++ b/packages/local_auth/local_auth/lib/error_codes.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. + +// Exception codes for `PlatformException` returned by +// `authenticate`. + +/// Indicates that the user has not yet configured a passcode (iOS) or +/// PIN/pattern/password (Android) on the device. +const String passcodeNotSet = 'PasscodeNotSet'; + +/// Indicates the user has not enrolled any biometrics on the device. +const String notEnrolled = 'NotEnrolled'; + +/// Indicates the device does not have hardware support for biometrics. +const String notAvailable = 'NotAvailable'; + +/// Indicates the device operating system is unsupported. +const String otherOperatingSystem = 'OtherOperatingSystem'; + +/// Indicates the API is temporarily locked out due to too many attempts. +const String lockedOut = 'LockedOut'; + +/// 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 new file mode 100644 index 000000000000..7c42fedc7755 --- /dev/null +++ b/packages/local_auth/local_auth/lib/local_auth.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. + +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 new file mode 100644 index 000000000000..a555150617b8 --- /dev/null +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -0,0 +1,39 @@ +name: local_auth +description: Flutter plugin for Android and iOS devices to allow local + authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. +repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 +version: 2.1.0 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + platforms: + android: + default_package: local_auth_android + ios: + default_package: local_auth_ios + windows: + default_package: local_auth_windows + +dependencies: + flutter: + sdk: flutter + intl: ^0.17.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: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + 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 new file mode 100644 index 000000000000..844c981e8120 --- /dev/null +++ b/packages/local_auth/local_auth/test/local_auth_test.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth/local_auth.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() { + WidgetsFlutterBinding.ensureInitialized(); + late LocalAuthentication localAuthentication; + late MockLocalAuthPlatform mockLocalAuthPlatform; + + setUp(() { + localAuthentication = LocalAuthentication(); + mockLocalAuthPlatform = MockLocalAuthPlatform(); + LocalAuthPlatform.instance = mockLocalAuthPlatform; + }); + + 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(), + ], + options: const AuthenticationOptions(), + )).called(1); + }); + + test('isDeviceSupported calls platform implementation', () { + when(mockLocalAuthPlatform.isDeviceSupported()) + .thenAnswer((_) async => true); + localAuthentication.isDeviceSupported(); + verify(mockLocalAuthPlatform.isDeviceSupported()).called(1); + }); + + test('getEnrolledBiometrics calls platform implementation', () { + when(mockLocalAuthPlatform.getEnrolledBiometrics()) + .thenAnswer((_) async => []); + localAuthentication.getAvailableBiometrics(); + verify(mockLocalAuthPlatform.getEnrolledBiometrics()).called(1); + }); + + test('stopAuthentication calls platform implementation', () { + when(mockLocalAuthPlatform.stopAuthentication()) + .thenAnswer((_) async => true); + localAuthentication.stopAuthentication(); + verify(mockLocalAuthPlatform.stopAuthentication()).called(1); + }); + + 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); + }); +} + +class MockLocalAuthPlatform extends Mock + with MockPlatformInterfaceMixin + implements LocalAuthPlatform { + MockLocalAuthPlatform() { + throwOnMissingStub(this); + } + + @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; + + @override + Future> getEnrolledBiometrics() => + super.noSuchMethod(Invocation.method(#getEnrolledBiometrics, []), + returnValue: Future>.value([])) + as Future>; + + @override + Future isDeviceSupported() => + super.noSuchMethod(Invocation.method(#isDeviceSupported, []), + returnValue: Future.value(false)) as Future; + + @override + Future stopAuthentication() => + super.noSuchMethod(Invocation.method(#stopAuthentication, []), + returnValue: Future.value(false)) as Future; + + @override + Future deviceSupportsBiometrics() => super.noSuchMethod( + Invocation.method(#deviceSupportsBiometrics, []), + returnValue: Future.value(false)) as Future; +} diff --git a/packages/local_auth/local_auth_android.iml b/packages/local_auth/local_auth_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/local_auth/local_auth_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - 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..99b337e667bc --- /dev/null +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -0,0 +1,32 @@ +## 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/in_app_purchase/in_app_purchase_ios/LICENSE b/packages/local_auth/local_auth_android/LICENSE similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/LICENSE rename to packages/local_auth/local_auth_android/LICENSE 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/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle new file mode 100644 index 000000000000..0486b8cea21a --- /dev/null +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -0,0 +1,60 @@ +group 'io.flutter.plugins.localauth' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.1' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + 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.13.2' + testImplementation 'org.mockito:mockito-inline:3.9.0' + 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/android/lint-baseline.xml b/packages/local_auth/local_auth_android/android/lint-baseline.xml similarity index 100% rename from packages/local_auth/android/lint-baseline.xml rename to packages/local_auth/local_auth_android/android/lint-baseline.xml diff --git a/packages/local_auth/android/settings.gradle b/packages/local_auth/local_auth_android/android/settings.gradle similarity index 100% rename from packages/local_auth/android/settings.gradle rename to packages/local_auth/local_auth_android/android/settings.gradle diff --git a/packages/local_auth/android/src/main/AndroidManifest.xml b/packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/local_auth/android/src/main/AndroidManifest.xml rename to packages/local_auth/local_auth_android/android/src/main/AndroidManifest.xml diff --git a/packages/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 100% rename from packages/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 diff --git a/packages/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 90% rename from packages/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 7ed9a7ea324d..3c5ecad16329 100644 --- a/packages/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,10 +11,10 @@ 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; import androidx.biometric.BiometricManager; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; @@ -39,7 +39,7 @@ */ @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); @@ -100,8 +100,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); @@ -109,6 +109,9 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) { case "stopAuthentication": stopAuthentication(result); break; + case "deviceSupportsBiometrics": + deviceSupportsBiometrics(result); + break; default: result.notImplemented(); break; @@ -247,42 +250,39 @@ 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() { + private 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; } @@ -337,6 +337,7 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { @Override public void onDetachedFromActivityForConfigChanges() { lifecycle = null; + activity = null; } @Override @@ -350,5 +351,16 @@ public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding public void onDetachedFromActivity() { lifecycle = null; channel.setMethodCallHandler(null); + activity = null; + } + + @VisibleForTesting + final Activity getActivity() { + return activity; + } + + @VisibleForTesting + void setBiometricManager(BiometricManager biometricManager) { + this.biometricManager = biometricManager; } } diff --git a/packages/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..5fbda46b984f --- /dev/null +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -0,0 +1,230 @@ +// Copyright 2013 The Flutter 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.ArgumentMatchers.anyInt; +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.biometric.BiometricManager; +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 java.util.ArrayList; +import java.util.Collections; +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 deviceSupportsBiometrics_returnsTrueForPresentNonEnrolledBiometrics() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + final BiometricManager mockBiometricManager = mock(BiometricManager.class); + when(mockBiometricManager.canAuthenticate()) + .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()).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()) + .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); + + 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 = buildMockActivity(); + 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, buildMockActivity()); + 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, buildMockActivity()); + 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, buildMockActivity()); + 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, buildMockActivity()); + 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"); + } + }); + } + + private Activity buildMockActivity() { + final Activity mockActivity = mock(Activity.class); + 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..bd004a77d86b --- /dev/null +++ b/packages/local_auth/local_auth_android/example/README.md @@ -0,0 +1,3 @@ +# local_auth_example + +Demonstrates how to use the local_auth plugin. 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..186b71557c50 --- /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-6.5-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/local_auth/local_auth_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java 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..8c091772107a --- /dev/null +++ b/packages/local_auth/local_auth_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/packages/device_info/device_info/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 similarity index 100% rename from packages/device_info/device_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/device_info/device_info/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 similarity index 100% rename from packages/device_info/device_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/device_info/device_info/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 similarity index 100% rename from packages/device_info/device_info/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/device_info/device_info/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 similarity index 100% rename from packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/device_info/device_info/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 similarity index 100% rename from packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/local_auth/local_auth_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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..54c943621de5 --- /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:4.1.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..cd9fe1c68282 --- /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-6.5-all.zip diff --git a/packages/local_auth/example/android/settings.gradle b/packages/local_auth/local_auth_android/example/android/settings.gradle similarity index 100% rename from packages/local_auth/example/android/settings.gradle rename to packages/local_auth/local_auth_android/example/android/settings.gradle diff --git a/packages/local_auth/example/android/settings_aar.gradle b/packages/local_auth/local_auth_android/example/android/settings_aar.gradle similarity index 100% rename from packages/local_auth/example/android/settings_aar.gradle rename to packages/local_auth/local_auth_android/example/android/settings_aar.gradle 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..016d955f0a3f --- /dev/null +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -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. + +// 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( + useErrorDialogs: true, + 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( + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true, + ), + ); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + 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..c07a81d2be3b --- /dev/null +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -0,0 +1,28 @@ +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.8.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 + 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..dfe785cc176f --- /dev/null +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors. 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_android/types/auth_messages_android.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.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..eb2638131ffb --- /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.6 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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.0 + +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..4b8e0653a7ad --- /dev/null +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -0,0 +1,34 @@ +## 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/LICENSE b/packages/local_auth/local_auth_ios/LICENSE similarity index 100% rename from packages/local_auth/LICENSE rename to packages/local_auth/local_auth_ios/LICENSE 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..bd004a77d86b --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/README.md @@ -0,0 +1,3 @@ +# local_auth_example + +Demonstrates how to use the local_auth plugin. 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/device_info/device_info/example/ios/Flutter/Debug.xcconfig b/packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/device_info/device_info/example/ios/Flutter/Debug.xcconfig rename to packages/local_auth/local_auth_ios/example/ios/Flutter/Debug.xcconfig diff --git a/packages/device_info/device_info/example/ios/Flutter/Release.xcconfig b/packages/local_auth/local_auth_ios/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/device_info/device_info/example/ios/Flutter/Release.xcconfig rename to packages/local_auth/local_auth_ios/example/ios/Flutter/Release.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/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/local_auth/local_auth_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata 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/package_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/package_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/local_auth/local_auth_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata 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/package_info/example/ios/Runner/AppDelegate.h b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/package_info/example/ios/Runner/AppDelegate.h rename to packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.h diff --git a/packages/package_info/example/ios/Runner/AppDelegate.m b/packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/package_info/example/ios/Runner/AppDelegate.m rename to packages/local_auth/local_auth_ios/example/ios/Runner/AppDelegate.m diff --git a/packages/local_auth/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 similarity index 100% rename from packages/local_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/package_info/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 similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/local_auth/local_auth_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/local_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/local_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/package_info/example/ios/Runner/Base.lproj/Main.storyboard b/packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/package_info/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/local_auth/local_auth_ios/example/ios/Runner/Base.lproj/Main.storyboard 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_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m new file mode 100644 index 000000000000..50dbb1a6907b --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -0,0 +1,477 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import LocalAuthentication; +@import XCTest; + +#import + +#if __has_include() +#import +#else +@import local_auth_ios; +#endif + +// Private API needed for tests. +@interface FLTLocalAuthPlugin (Test) +- (void)setAuthContextOverrides:(NSArray *)authContexts; +@end + +// Set a long timeout to avoid flake due to slow CI. +static const NSTimeInterval kTimeout = 30.0; + +@interface FLTLocalAuthPluginTests : XCTestCase +@end + +@implementation FLTLocalAuthPluginTests + +- (void)setUp { + self.continueAfterFailure = NO; +} + +- (void)testSuccessfullAuthWithBiometrics { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(YES, nil); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(YES), + @"localizedReason" : reason, + }]; + + 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)testSuccessfullAuthWithoutBiometrics { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(YES, nil); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + }]; + + 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)testFailedAuthWithBiometrics { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(YES), + @"localizedReason" : reason, + }]; + + 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)testFailedAuthWithoutBiometrics { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + }]; + + 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)testLocalizedFallbackTitle { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + NSString *localizedFallbackTitle = @"a title"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + @"localizedFallbackTitle" : localizedFallbackTitle, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testSkippedLocalizedFallbackTitle { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[NSNumber class]]); + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + [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/share/example/ios/RunnerUITests/Info.plist b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/share/example/ios/RunnerUITests/Info.plist rename to packages/local_auth/local_auth_ios/example/ios/RunnerTests/Info.plist 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..479a96ba809c --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -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. + +// 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( + useErrorDialogs: true, + 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( + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true, + ), + ); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + 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..f83806b9d08e --- /dev/null +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -0,0 +1,28 @@ +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.8.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 + 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/device_info/device_info/ios/Assets/.gitkeep b/packages/local_auth/local_auth_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/device_info/device_info/ios/Assets/.gitkeep rename to packages/local_auth/local_auth_ios/ios/Assets/.gitkeep diff --git a/packages/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/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/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m new file mode 100644 index 000000000000..8f61fecfd814 --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -0,0 +1,288 @@ +// Copyright 2013 The Flutter Authors. 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 "FLTLocalAuthPlugin.h" + +@interface FLTLocalAuthPlugin () +@property(nonatomic, copy, nullable) NSDictionary *lastCallArgs; +@property(nonatomic, nullable) FlutterResult lastResult; +// For unit tests to inject dummy LAContext instances that will be used when a new context would +// normally be created. Each call to createAuthContext will remove the current first element from +// the array. +- (void)setAuthContextOverrides:(NSArray *)authContexts; +@end + +@implementation FLTLocalAuthPlugin { + NSMutableArray *_authContextOverrides; +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/local_auth_ios" + binaryMessenger:[registrar messenger]]; + FLTLocalAuthPlugin *instance = [[FLTLocalAuthPlugin alloc] init]; + [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"authenticate" isEqualToString:call.method]) { + bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; + if (isBiometricOnly) { + [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + } else { + [self authenticate:call.arguments withFlutterResult:result]; + } + } else if ([@"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 { + result(FlutterMethodNotImplemented); + } +} + +#pragma mark Private Methods + +- (void)setAuthContextOverrides:(NSArray *)authContexts { + _authContextOverrides = [authContexts mutableCopy]; +} + +- (LAContext *)createAuthContext { + if ([_authContextOverrides count] > 0) { + LAContext *context = [_authContextOverrides firstObject]; + [_authContextOverrides removeObjectAtIndex:0]; + return context; + } + return [[LAContext alloc] init]; +} + +- (void)alertMessage:(NSString *)message + firstButton:(NSString *)firstButton + flutterResult:(FlutterResult)result + additionalButton:(NSString *)secondButton { + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:firstButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + result(@NO); + }]; + + [alert addAction:defaultAction]; + if (secondButton != nil) { + UIAlertAction *additionalAction = [UIAlertAction + actionWithTitle:secondButton + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + if (UIApplicationOpenSettingsURLString != NULL) { + NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; + 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); + } + }]; + [alert addAction:additionalAction]; + } + [[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:alert + animated:YES + completion:nil]; +} + +- (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, *)) { + if (context.biometryType == LABiometryTypeFaceID) { + [biometrics addObject:@"face"]; + } else if (context.biometryType == LABiometryTypeTouchID) { + [biometrics addObject:@"fingerprint"]; + } + } else { + [biometrics addObject:@"fingerprint"]; + } + } + } + result(biometrics); +} + +- (void)authenticateWithBiometrics:(NSDictionary *)arguments + withFlutterResult:(FlutterResult)result { + LAContext *context = self.createAuthContext; + NSError *authError = nil; + self.lastCallArgs = nil; + self.lastResult = nil; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&authError]) { + [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } +} + +- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { + LAContext *context = self.createAuthContext; + NSError *authError = nil; + _lastCallArgs = nil; + _lastResult = nil; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } +} + +- (void)handleAuthReplyWithSuccess:(BOOL)success + error:(NSError *)error + flutterArguments:(NSDictionary *)arguments + flutterResult:(FlutterResult)result { + NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); + if (success) { + result(@YES); + } 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 LAErrorTouchIDLockout: +#pragma clang diagnostic pop + case LAErrorUserFallback: + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + return; + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + return; + } + } + result(@NO); + } +} + +- (void)handleErrors:(NSError *)authError + flutterArguments:(NSDictionary *)arguments + withFlutterResult:(FlutterResult)result { + 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"] + flutterResult:result + additionalButton:arguments[@"goToSetting"]]; + return; + } + 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 + additionalButton:nil]; + return; + } + result([FlutterError errorWithCode:errorCode + message:authError.localizedDescription + details:authError.domain]); +} + +#pragma mark - AppDelegate + +- (void)applicationDidBecomeActive:(UIApplication *)application { + if (self.lastCallArgs != nil && self.lastResult != nil) { + [self authenticateWithBiometrics:_lastCallArgs withFlutterResult:self.lastResult]; + } +} + +@end diff --git a/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec b/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec new file mode 100644 index 000000000000..0828c6085ea2 --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/local_auth_ios.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'local_auth_ios' + s.version = '0.0.1' + s.summary = 'Flutter Local Auth' + s.description = <<-DESC +This Flutter plugin provides means to perform local, on-device authentication of the user. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/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' + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end + 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..d9df89a656a8 --- /dev/null +++ b/packages/local_auth/local_auth_ios/lib/local_auth_ios.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/services.dart'; +import 'package:local_auth_ios/types/auth_messages_ios.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.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..043d84eb4a2c --- /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.7 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.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.0 + +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/AUTHORS b/packages/local_auth/local_auth_platform_interface/AUTHORS new file mode 100644 index 000000000000..d5694690c247 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/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_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..387a20050ed8 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -0,0 +1,22 @@ +## 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/package_info/LICENSE b/packages/local_auth/local_auth_platform_interface/LICENSE similarity index 100% rename from packages/package_info/LICENSE rename to packages/local_auth/local_auth_platform_interface/LICENSE diff --git a/packages/local_auth/local_auth_platform_interface/README.md b/packages/local_auth/local_auth_platform_interface/README.md new file mode 100644 index 000000000000..3b01ced7b93b --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/README.md @@ -0,0 +1,26 @@ +# local_auth_platform_interface + +A common platform interface for the [`local_auth`][1] plugin. + +This interface allows platform-specific implementations of the `local_auth` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `local_auth`, extend +[`LocalAuthPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`LocalAuthPlatform` by calling +`LocalAuthPlatform.instance = MyLocalAuthPlatform()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../local_auth +[2]: lib/local_auth_platform_interface.dart 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 new file mode 100644 index 000000000000..9ded078c3a90 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/default_method_channel_platform.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'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +const MethodChannel _channel = MethodChannel('plugins.flutter.io/local_auth'); + +/// The default interface implementation acting as a placeholder for +/// the native implementation to be set. +/// +/// This implementation is not used by any of the implementations in this +/// repository, and exists only for backward compatibility with any +/// clients that were relying on internal details of the method channel +/// in the pre-federated plugin. +class DefaultLocalAuthPlatform extends LocalAuthPlatform { + @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, + }; + for (final AuthMessages messages in authMessages) { + args.addAll(messages.args); + } + return (await _channel.invokeMethod('authenticate', args)) ?? false; + } + + @override + Future> getEnrolledBiometrics() 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': + // Sentinel value for the case when nothing is enrolled, but hardware + // support for biometrics is available. + break; + } + } + return biometrics; + } + + @override + Future deviceSupportsBiometrics() async { + 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 + 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_platform_interface/lib/local_auth_platform_interface.dart b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart new file mode 100644 index 000000000000..de652b20f462 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/local_auth_platform_interface.dart @@ -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. + +import 'package:local_auth_platform_interface/default_method_channel_platform.dart'; +import 'package:local_auth_platform_interface/types/types.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.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` +/// 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 +/// [LocalAuthPlatform] methods. +abstract class LocalAuthPlatform extends PlatformInterface { + /// Constructs a LocalAuthPlatform. + LocalAuthPlatform() : super(token: _token); + + static final Object _token = Object(); + + static LocalAuthPlatform _instance = DefaultLocalAuthPlatform(); + + /// The default instance of [LocalAuthPlatform] to use. + /// + /// Defaults to [DefaultLocalAuthPlatform]. + static LocalAuthPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [LocalAuthPlatform] when they + /// register themselves. + static set instance(LocalAuthPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// 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: 'Please scan + /// your finger 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, + required Iterable authMessages, + AuthenticationOptions options = const AuthenticationOptions(), + }) async { + throw UnimplementedError('authenticate() has not been implemented.'); + } + + /// Returns true if the device is capable of checking biometrics. + /// + /// This will return true even if there are no biometrics currently enrolled. + Future deviceSupportsBiometrics() async { + throw UnimplementedError('canCheckBiometrics() has not been implemented.'); + } + + /// Returns a list of enrolled biometrics. + /// + /// Possible values include: + /// - BiometricType.face + /// - BiometricType.fingerprint + /// - BiometricType.iris (not yet implemented) + /// - BiometricType.strong + /// - BiometricType.weak + Future> getEnrolledBiometrics() async { + throw UnimplementedError( + 'getAvailableBiometrics() has not been implemented.'); + } + + /// Returns true if device is capable of checking biometrics or is able to + /// fail over to device credentials. + Future isDeviceSupported() async { + throw UnimplementedError('isDeviceSupported() has not been implemented.'); + } + + /// Cancels any authentication currently in progress. + /// + /// Returns true if auth was cancelled successfully. + /// Returns false if there was no authentication in progress, + /// or an error occurred. + Future stopAuthentication() async { + throw UnimplementedError('stopAuthentication() has not been implemented.'); + } +} diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.dart b/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.dart new file mode 100644 index 000000000000..d51980d575cf --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_messages.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. + +/// Abstract class for storing platform specific strings. +abstract class AuthMessages { + /// Constructs an instance of [AuthMessages]. + const AuthMessages(); + + /// Returns all platform-specific messages as a map. + Map get args; +} 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 new file mode 100644 index 000000000000..a5af8e73a640 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/types/auth_options.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 'package:flutter/foundation.dart'; + +/// Options wrapper for [LocalAuthPlatform.authenticate] parameters. +@immutable +class AuthenticationOptions { + /// Constructs a new instance. + const AuthenticationOptions({ + this.useErrorDialogs = true, + this.stickyAuth = false, + this.sensitiveTransaction = true, + this.biometricOnly = false, + }); + + /// Whether the system will attempt to handle user-fixable issues encountered + /// while authenticating. For instance, if a fingerprint reader exists on the + /// device but there's no fingerprint registered, the plugin might attempt to + /// take the user to settings to add one. Anything that is not user fixable, + /// such as no biometric sensor on device, will still result in + /// a [PlatformException]. + final bool useErrorDialogs; + + /// 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. + final bool stickyAuth; + + /// Whether platform specific precautions are enabled. For instance, on face + /// unlock, Android opens a confirmation dialog after the face is recognized + /// to make sure the user meant to unlock their device. + final bool sensitiveTransaction; + + /// Prevent authentications from using non-biometric local authentication + /// such as pin, passcode, or pattern. + final bool biometricOnly; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AuthenticationOptions && + runtimeType == other.runtimeType && + useErrorDialogs == other.useErrorDialogs && + stickyAuth == other.stickyAuth && + sensitiveTransaction == other.sensitiveTransaction && + biometricOnly == other.biometricOnly; + + @override + int get hashCode => Object.hash( + useErrorDialogs, + stickyAuth, + sensitiveTransaction, + biometricOnly, + ); +} diff --git a/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart b/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart new file mode 100644 index 000000000000..9c335e25624a --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/lib/types/biometric_type.dart @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Various types of biometric authentication. +/// Some platforms report specific biometric types, while others report only +/// classifications like strong and weak. +enum BiometricType { + /// Face authentication. + face, + + /// Fingerprint authentication. + fingerprint, + + /// Iris authentication. + iris, + + /// Any biometric (e.g. fingerprint, iris, or face) on the device that the + /// platform API considers to be strong. For example, on Android this + /// corresponds to Class 3. + strong, + + /// Any biometric (e.g. fingerprint, iris, or face) on the device that the + /// platform API considers to be weak. For example, on Android this + /// corresponds to Class 2. + weak, +} 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 new file mode 100644 index 000000000000..a4ad682b363d --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: local_auth_platform_interface +description: A common platform interface for the local_auth plugin. +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.4 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + intl: ^0.17.0 + plugin_platform_interface: ^2.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + 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 new file mode 100644 index 000000000000..824597ab2953 --- /dev/null +++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart @@ -0,0 +1,200 @@ +// Copyright 2013 The Flutter Authors. 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_platform_interface/default_method_channel_platform.dart'; +import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/local_auth', + ); + + late List log; + late LocalAuthPlatform localAuthentication; + + setUp(() async { + log = []; + }); + + test( + 'DefaultLocalAuthPlatform is registered as the default platform implementation', + () async { + expect(LocalAuthPlatform.instance, + const TypeMatcher()); + }); + + test('getAvailableBiometrics', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall); + return Future.value([]); + }); + localAuthentication = DefaultLocalAuthPlatform(); + await localAuthentication.getEnrolledBiometrics(); + expect( + log, + [ + isMethodCall('getAvailableBiometrics', arguments: null), + ], + ); + }); + + 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) { + log.add(methodCall); + return Future.value(true); + }); + localAuthentication = DefaultLocalAuthPlatform(); + }); + + test('isDeviceSupported', () async { + await localAuthentication.isDeviceSupported(); + expect( + log, + [ + isMethodCall('isDeviceSupported', arguments: null), + ], + ); + }); + + test('stopAuthentication', () async { + await localAuthentication.stopAuthentication(); + expect( + log, + [ + isMethodCall('stopAuthentication', arguments: null), + ], + ); + }); + + group('authenticate with device auth fail over', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [], + localizedReason: 'Needs secure', + options: const AuthenticationOptions(biometricOnly: true), + ); + expect( + log, + [ + isMethodCall( + 'authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }, + ), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [], + 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, + }, + ), + ], + ); + }); + }); + + group('authenticate with biometrics only', () { + test('authenticate with no args.', () async { + await localAuthentication.authenticate( + authMessages: [], + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall( + 'authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }, + ), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + await localAuthentication.authenticate( + authMessages: [], + localizedReason: 'Insecure', + options: const AuthenticationOptions( + sensitiveTransaction: false, + useErrorDialogs: false, + ), + ); + expect( + log, + [ + isMethodCall( + 'authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }, + ), + ], + ); + }); + }); + }); +} 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..f6c5e909b31e --- /dev/null +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -0,0 +1,7 @@ +## 1.0.1 + +* Updates references to the obsolete master branch. + +## 1.0.0 + +* Initial release of Windows support. diff --git a/packages/sensors/LICENSE b/packages/local_auth/local_auth_windows/LICENSE similarity index 100% rename from packages/sensors/LICENSE rename to packages/local_auth/local_auth_windows/LICENSE 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..8f48b8563cad --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/README.md @@ -0,0 +1,3 @@ +# local_auth_example + +Demonstrates how to use the local_auth plugin. \ No newline at end of file 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..ef26ec5545c5 --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -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. + +// 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( + useErrorDialogs: true, + 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( + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true, + ), + ); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = 'Error - ${e.message}'; + }); + return; + } + if (!mounted) { + return; + } + + 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..266c9fc7140d --- /dev/null +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -0,0 +1,28 @@ +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.8.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 + 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..1d65e81050f1 --- /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 'package:local_auth_windows/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..b42a4f846cc3 --- /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.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: local_auth + platforms: + windows: + pluginClass: LocalAuthPlugin + dartPluginClass: LocalAuthWindows + +dependencies: + flutter: + sdk: flutter + local_auth_platform_interface: ^1.0.0 + +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/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml deleted file mode 100644 index 4f5ef26d9fb1..000000000000 --- a/packages/local_auth/pubspec.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: local_auth -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/master/packages/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.8 - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.localauth - pluginClass: LocalAuthPlugin - ios: - pluginClass: FLTLocalAuthPlugin - -dependencies: - flutter: - sdk: flutter - flutter_plugin_android_lifecycle: ^2.0.1 - intl: ^0.17.0 - meta: ^1.3.0 - platform: ^3.0.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/local_auth/test/local_auth_test.dart b/packages/local_auth/test/local_auth_test.dart deleted file mode 100644 index b24de8bd3c11..000000000000 --- a/packages/local_auth/test/local_auth_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.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'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('LocalAuth', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth', - ); - - final List log = []; - late LocalAuthentication localAuthentication; - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - return Future.value(true); - }); - localAuthentication = LocalAuthentication(); - log.clear(); - }); - - group("With device auth fail over", () { - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - }..addAll(const AndroidAuthMessages().args)), - ], - ); - }); - - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with no localizedReason on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await expectLater( - localAuthentication.authenticate( - localizedReason: '', - biometricOnly: true, - ), - throwsAssertionError, - ); - }); - - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - biometricOnly: true, - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': true, - }..addAll(const AndroidAuthMessages().args)), - ], - ); - }); - }); - - group("With biometrics only", () { - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const AndroidAuthMessages().args)), - ], - ); - }); - - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticate( - localizedReason: 'Needs secure', - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const IOSAuthMessages().args)), - ], - ); - }); - - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticate( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - ); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': false, - }..addAll(const AndroidAuthMessages().args)), - ], - ); - }); - }); - }); -} diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md deleted file mode 100644 index 0fe91175cf6b..000000000000 --- a/packages/package_info/CHANGELOG.md +++ /dev/null @@ -1,187 +0,0 @@ -## NEXT - -* Remove references to the Android v1 embedding. -* Updated Android lint settings. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 0.4.3+4 - -* Ensure `IntegrationTestPlugin` is registered in `example` app, so Firebase Test Lab tests report test results correctly. [Issue](https://github.com/flutter/flutter/issues/74944). - -## 0.4.3+3 - -* Update Flutter SDK constraint. - -## 0.4.3+2 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 0.4.3+1 - -* Update android compileSdkVersion to 29. - -## 0.4.3 - -* Update package:e2e -> package:integration_test - -## 0.4.2 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.1 - -* Add support for macOS. - -## 0.4.0+18 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.0+17 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Clean up various Android workarounds no longer needed after framework v1.12. -* Complete v2 embedding support. -* Fix CocoaPods podspec lint warnings. - -## 0.4.0+16 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.0+15 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.0+14 - -* Make the pedantic dev_dependency explicit. - -## 0.4.0+13 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.0+12 - -* Fix pedantic lints. This involved internally refactoring how the - `PackageInfo.fromPlatform` code handled futures, but shouldn't change existing - functionality. - -## 0.4.0+11 - -* Remove AndroidX warnings. - -## 0.4.0+10 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.0+9 - -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. - -## 0.4.0+8 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Add a unit test. -* Migrate to using the new e2e test binding. - -## 0.4.0+7 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.4.0+6 - -* Fix Android compiler warnings. - -## 0.4.0+5 - -* Add iOS-specific warning to README.md. - -## 0.4.0+4 - -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.4.0+3 - -* Add integration test. - -## 0.4.0+2 - -* Android: Using new method for BuildNumber in new android versions - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.2+1 - -* Fixed a crash on IOS when some of the package infos are not available. - -## 0.3.2 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.3.1 - -* Added `appName` field to `PackageInfo` for getting the display name of the app. - -## 0.3.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.2.1 - -* Fixed Dart 2 type error. - -## 0.2.0 - -* **Breaking change**. Introduced class `PackageInfo` in place of individual functions. -* `PackageInfo` provides all package information with a single async call. - -## 0.1.1 - -* Added package name to available information. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.2 - -* Add FLT prefix to iOS types - -## 0.0.1 - -* Initial release diff --git a/packages/package_info/README.md b/packages/package_info/README.md deleted file mode 100644 index 80893880f3c2..000000000000 --- a/packages/package_info/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# PackageInfo - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`package_info_plus`](https://pub.dev/packages/package_info_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -This Flutter plugin provides an API for querying information about an -application package. - -# Usage - -You can use the PackageInfo to query information about the -application package. This works both on iOS and Android. - -```dart -import 'package:package_info/package_info.dart'; - -PackageInfo packageInfo = await PackageInfo.fromPlatform(); - -String appName = packageInfo.appName; -String packageName = packageInfo.packageName; -String version = packageInfo.version; -String buildNumber = packageInfo.buildNumber; -``` - -Or in async mode: - -```dart -PackageInfo.fromPlatform().then((PackageInfo packageInfo) { - String appName = packageInfo.appName; - String packageName = packageInfo.packageName; - String version = packageInfo.version; - String buildNumber = packageInfo.buildNumber; -}); -``` - -## Known Issue - -As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS -requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. -Clean the Xcode build folder with: -`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. - -## Issues and feedback - -Please file [issues](https://github.com/flutter/flutter/issues/new) to send feedback or report a bug. Thank you! diff --git a/packages/package_info/analysis_options.yaml b/packages/package_info/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/package_info/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle deleted file mode 100644 index e21d911ff490..000000000000 --- a/packages/package_info/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.packageinfo' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/package_info/android/settings.gradle b/packages/package_info/android/settings.gradle deleted file mode 100644 index a5683f94fce7..000000000000 --- a/packages/package_info/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'package_info' diff --git a/packages/package_info/android/src/main/AndroidManifest.xml b/packages/package_info/android/src/main/AndroidManifest.xml deleted file mode 100644 index 133ae5faf3c7..000000000000 --- a/packages/package_info/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java b/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java deleted file mode 100644 index 4611f70951f9..000000000000 --- a/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java +++ /dev/null @@ -1,79 +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.packageinfo; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.Build; -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 java.util.HashMap; -import java.util.Map; - -/** PackageInfoPlugin */ -public class PackageInfoPlugin implements MethodCallHandler, FlutterPlugin { - private Context applicationContext; - private MethodChannel methodChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - final PackageInfoPlugin instance = new PackageInfoPlugin(); - instance.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { - this.applicationContext = applicationContext; - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/package_info"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - applicationContext = null; - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - try { - if (call.method.equals("getAll")) { - PackageManager pm = applicationContext.getPackageManager(); - PackageInfo info = pm.getPackageInfo(applicationContext.getPackageName(), 0); - - Map map = new HashMap<>(); - map.put("appName", info.applicationInfo.loadLabel(pm).toString()); - map.put("packageName", applicationContext.getPackageName()); - map.put("version", info.versionName); - map.put("buildNumber", String.valueOf(getLongVersionCode(info))); - - result.success(map); - } else { - result.notImplemented(); - } - } catch (PackageManager.NameNotFoundException ex) { - result.error("Name not found", ex.getMessage(), null); - } - } - - @SuppressWarnings("deprecation") - private static long getLongVersionCode(PackageInfo info) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return info.getLongVersionCode(); - } - return info.versionCode; - } -} diff --git a/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m b/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m deleted file mode 100644 index ab686fa08676..000000000000 --- a/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTPackageInfoPlugin.h" - -@implementation FLTPackageInfoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/package_info" - binaryMessenger:[registrar messenger]]; - FLTPackageInfoPlugin* instance = [[FLTPackageInfoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"getAll"]) { - result(@{ - @"appName" : [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] - ?: [NSNull null], - @"packageName" : [[NSBundle mainBundle] bundleIdentifier] ?: [NSNull null], - @"version" : [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] - ?: [NSNull null], - @"buildNumber" : [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] - ?: [NSNull null], - }); - } else { - result(FlutterMethodNotImplemented); - } -} - -@end diff --git a/packages/package_info/example/README.md b/packages/package_info/example/README.md deleted file mode 100644 index 762d04ec0532..000000000000 --- a/packages/package_info/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# package_info_example - -Demonstrates how to use the package_info plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/package_info/example/android.iml b/packages/package_info/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/package_info/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/package_info/example/android/app/build.gradle b/packages/package_info/example/android/app/build.gradle deleted file mode 100644 index 5099f3213fd8..000000000000 --- a/packages/package_info/example/android/app/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.packageinfoexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java deleted file mode 100644 index fb63f6f8c88b..000000000000 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java +++ /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. - -package io.flutter.plugins.packageinfoexample; - -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/package_info/example/android/app/src/main/AndroidManifest.xml b/packages/package_info/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index efb42ac02c5c..000000000000 --- a/packages/package_info/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/package_info/example/android/build.gradle b/packages/package_info/example/android/build.gradle deleted file mode 100644 index 64450a26d537..000000000000 --- a/packages/package_info/example/android/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/package_info/example/android/gradle.properties b/packages/package_info/example/android/gradle.properties deleted file mode 100644 index 05413bc45d00..000000000000 --- a/packages/package_info/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableJetifier=true -android.enableR8=true -android.useAndroidX=true diff --git a/packages/package_info/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/package_info/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/package_info/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -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/package_info/example/integration_test/package_info_test.dart b/packages/package_info/example/integration_test/package_info_test.dart deleted file mode 100644 index ab8f5f38b472..000000000000 --- a/packages/package_info/example/integration_test/package_info_test.dart +++ /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. - -// @dart=2.9 - -import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:package_info/package_info.dart'; -import 'package:package_info_example/main.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('fromPlatform', (WidgetTester tester) async { - final PackageInfo info = await PackageInfo.fromPlatform(); - // These tests are based on the example app. The tests should be updated if any related info changes. - if (Platform.isAndroid) { - expect(info.appName, 'package_info_example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); - expect(info.version, '1.0'); - } else if (Platform.isIOS) { - expect(info.appName, 'Package Info Example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); - expect(info.version, '1.0'); - } else if (Platform.isMacOS) { - expect(info.appName, 'Package Info Example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); - expect(info.version, '1.0.0'); - } else { - throw (UnsupportedError('platform not supported')); - } - }); - - testWidgets('example', (WidgetTester tester) async { - await tester.pumpWidget(MyApp()); - await tester.pumpAndSettle(); - if (Platform.isAndroid) { - expect(find.text('package_info_example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('io.flutter.plugins.packageinfoexample'), findsOneWidget); - expect(find.text('1.0'), findsOneWidget); - } else if (Platform.isIOS) { - expect(find.text('Package Info Example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('dev.flutter.plugins.packageInfoExample'), findsOneWidget); - expect(find.text('1.0'), findsOneWidget); - } else if (Platform.isMacOS) { - expect(find.text('Package Info Example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('dev.flutter.plugins.packageInfoExample'), findsOneWidget); - expect(find.text('1.0.0'), findsOneWidget); - } else { - throw (UnsupportedError('platform not supported')); - } - }); -} diff --git a/packages/package_info/example/ios/Flutter/AppFrameworkInfo.plist b/packages/package_info/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/package_info/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/package_info/example/ios/Flutter/Debug.xcconfig b/packages/package_info/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/package_info/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/package_info/example/ios/Flutter/Release.xcconfig b/packages/package_info/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/package_info/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/package_info/example/ios/Podfile b/packages/package_info/example/ios/Podfile deleted file mode 100644 index f7d6a5e68c3a..000000000000 --- a/packages/package_info/example/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# 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__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj b/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index f21209190faa..000000000000 --- a/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,460 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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 */; }; - C824856099B606661DF36830 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D007BB586407934FC28AF83 /* libPods-Runner.a */; }; -/* 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 */ - 0B4CD678E84FD9C2C80D895C /* 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 = ""; }; - 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 = ""; }; - 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 = ""; }; - 7D007BB586407934FC28AF83 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 8F88DBCB0DD2793F05ADE394 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C824856099B606661DF36830 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1B8D0C5C4E228D9E0271D922 /* Pods */ = { - isa = PBXGroup; - children = ( - 0B4CD678E84FD9C2C80D895C /* Pods-Runner.debug.xcconfig */, - 8F88DBCB0DD2793F05ADE394 /* Pods-Runner.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 = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 1B8D0C5C4E228D9E0271D922 /* Pods */, - F2F265795CE7F5960E889E92 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - F2F265795CE7F5960E889E92 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7D007BB586407934FC28AF83 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AA8267A332E7FFF199F5E510 /* [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 = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" 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"; - }; - AA8267A332E7FFF199F5E510 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample; - 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.packageInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/package_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/package_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/package_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/ios/Runner/Info.plist b/packages/package_info/example/ios/Runner/Info.plist deleted file mode 100644 index 0ba5e95cf616..000000000000 --- a/packages/package_info/example/ios/Runner/Info.plist +++ /dev/null @@ -1,51 +0,0 @@ - - - - - CFBundleDisplayName - Package Info Example - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - package_info_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/package_info/example/ios/Runner/main.m b/packages/package_info/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/package_info/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/package_info/example/lib/main.dart b/packages/package_info/example/lib/main.dart deleted file mode 100644 index 60e4a16f7817..000000000000 --- a/packages/package_info/example/lib/main.dart +++ /dev/null @@ -1,81 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:package_info/package_info.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'PackageInfo Demo', - theme: ThemeData(primarySwatch: Colors.blue), - home: MyHomePage(title: 'PackageInfo example app'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - PackageInfo _packageInfo = PackageInfo( - appName: 'Unknown', - packageName: 'Unknown', - version: 'Unknown', - buildNumber: 'Unknown', - ); - - @override - void initState() { - super.initState(); - _initPackageInfo(); - } - - Future _initPackageInfo() async { - final PackageInfo info = await PackageInfo.fromPlatform(); - setState(() { - _packageInfo = info; - }); - } - - Widget _infoTile(String title, String subtitle) { - return ListTile( - title: Text(title), - subtitle: Text(subtitle.isNotEmpty ? subtitle : 'Not set'), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _infoTile('App name', _packageInfo.appName), - _infoTile('Package name', _packageInfo.packageName), - _infoTile('App version', _packageInfo.version), - _infoTile('Build number', _packageInfo.buildNumber), - ], - ), - ); - } -} diff --git a/packages/package_info/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/package_info/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/package_info/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/package_info/example/macos/Flutter/Flutter-Release.xcconfig b/packages/package_info/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/package_info/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj b/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 3525d85d6678..000000000000 --- a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,644 +0,0 @@ -// !$*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 */; }; - 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED91D820ABAEDEBEFEBDBDA /* 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 */ - 26D1A257E90EADFCCC251DEE /* 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 = ""; }; - 2DB1F806EBAC0EF2659B294F /* 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 = ""; }; - 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 /* Package Info Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Package Info 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 = ""; }; - B3868D4F5169B9990BB5D1F5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9A0CC0B8F23AFE5DF719BADB /* 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 */, - 58201D283908D33A078698CD /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* Package Info 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 = ""; - }; - 58201D283908D33A078698CD /* Pods */ = { - isa = PBXGroup; - children = ( - 26D1A257E90EADFCCC251DEE /* Pods-Runner.debug.xcconfig */, - B3868D4F5169B9990BB5D1F5 /* Pods-Runner.release.xcconfig */, - 2DB1F806EBAC0EF2659B294F /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - CED91D820ABAEDEBEFEBDBDA /* 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 = ( - A7B87C53ACB98DD8DB15DE02 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - F39B8515B2FA626EA800A9B8 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* Package Info Example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "The Flutter Authors"; - 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 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - 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\ntouch Flutter/ephemeral/tripwire\n"; - }; - A7B87C53ACB98DD8DB15DE02 /* [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; - }; - F39B8515B2FA626EA800A9B8 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", - ); - 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 */ - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); - 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/package_info/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/package_info/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 9c27c87c30ba..000000000000 --- a/packages/package_info/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/macos/Runner/AppDelegate.swift b/packages/package_info/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index 5cec4c48f620..000000000000 --- a/packages/package_info/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f11..000000000000 --- a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "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/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7ca84..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc1642168..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be6138973..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2f2a..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a65286476..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726136a4..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2dd3f9..000000000000 Binary files a/packages/package_info/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/packages/package_info/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/package_info/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 537341abf994..000000000000 --- a/packages/package_info/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 4ecd23f86c29..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// 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 = Package Info Example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2020 The Flutter Authors. All rights reserved. diff --git a/packages/package_info/example/macos/Runner/Configs/Debug.xcconfig b/packages/package_info/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464f4..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/package_info/example/macos/Runner/Configs/Release.xcconfig b/packages/package_info/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561c8..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/package_info/example/macos/Runner/Configs/Warnings.xcconfig b/packages/package_info/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780b1..000000000000 --- a/packages/package_info/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -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/package_info/example/macos/Runner/DebugProfile.entitlements b/packages/package_info/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/package_info/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/packages/package_info/example/macos/Runner/Info.plist b/packages/package_info/example/macos/Runner/Info.plist deleted file mode 100644 index a8e4f02255ba..000000000000 --- a/packages/package_info/example/macos/Runner/Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - 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 - CFBundleDisplayName - $(PRODUCT_NAME) - - diff --git a/packages/package_info/example/macos/Runner/MainFlutterWindow.swift b/packages/package_info/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 32aaeedceb1f..000000000000 --- a/packages/package_info/example/macos/Runner/MainFlutterWindow.swift +++ /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 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/package_info/example/macos/Runner/Release.entitlements b/packages/package_info/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/package_info/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/package_info/example/package_info_example.iml b/packages/package_info/example/package_info_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/package_info/example/package_info_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/package_info/example/package_info_example_android.iml b/packages/package_info/example/package_info_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/package_info/example/package_info_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/package_info/example/pubspec.yaml b/packages/package_info/example/pubspec.yaml deleted file mode 100644 index 9c1c3cd0ad6e..000000000000 --- a/packages/package_info/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: package_info_example -description: Demonstrates how to use the package_info plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - package_info: - # When depending on this package from a real application you should use: - # package_info: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - integration_test: - sdk: flutter - -dev_dependencies: - flutter_driver: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/package_info/example/test_driver/integration_test.dart b/packages/package_info/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/package_info/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h b/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h deleted file mode 100644 index 65be5f99d569..000000000000 --- a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTPackageInfoPlugin : NSObject -@end diff --git a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.m b/packages/package_info/ios/Classes/FLTPackageInfoPlugin.m deleted file mode 120000 index a1ddabd76793..000000000000 --- a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.m +++ /dev/null @@ -1 +0,0 @@ -../../darwin/Classes/FLTPackageInfoPlugin.m \ No newline at end of file diff --git a/packages/package_info/ios/package_info.podspec b/packages/package_info/ios/package_info.podspec deleted file mode 100644 index 12ccab3b855b..000000000000 --- a/packages/package_info/ios/package_info.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'package_info' - s.version = '0.0.1' - s.summary = 'Flutter Package Info' - s.description = <<-DESC -This Flutter plugin provides an API for querying information about an application package. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/package_info' } - s.documentation_url = 'https://pub.dev/packages/package_info' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/package_info/lib/package_info.dart b/packages/package_info/lib/package_info.dart deleted file mode 100644 index 69246813873a..000000000000 --- a/packages/package_info/lib/package_info.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 'dart:async'; - -import 'package:flutter/services.dart'; - -const MethodChannel _kChannel = - MethodChannel('plugins.flutter.io/package_info'); - -/// Application metadata. Provides application bundle information on iOS and -/// application package information on Android. -/// -/// ```dart -/// PackageInfo packageInfo = await PackageInfo.fromPlatform() -/// print("Version is: ${packageInfo.version}"); -/// ``` -class PackageInfo { - /// Constructs an instance with the given values for testing. [PackageInfo] - /// instances constructed this way won't actually reflect any real information - /// from the platform, just whatever was passed in at construction time. - /// - /// See [fromPlatform] for the right API to get a [PackageInfo] that's - /// actually populated with real data. - PackageInfo({ - required this.appName, - required this.packageName, - required this.version, - required this.buildNumber, - }); - - static PackageInfo? _fromPlatform; - - /// Retrieves package information from the platform. - /// The result is cached. - static Future fromPlatform() async { - PackageInfo? packageInfo = _fromPlatform; - if (packageInfo != null) return packageInfo; - - final Map map = - (await _kChannel.invokeMapMethod('getAll'))!; - - packageInfo = PackageInfo( - appName: map["appName"] ?? '', - packageName: map["packageName"] ?? '', - version: map["version"] ?? '', - buildNumber: map["buildNumber"] ?? '', - ); - _fromPlatform = packageInfo; - return packageInfo; - } - - /// The app name. `CFBundleDisplayName` on iOS, `application/label` on Android. - final String appName; - - /// The package name. `bundleIdentifier` on iOS, `getPackageName` on Android. - final String packageName; - - /// The package version. `CFBundleShortVersionString` on iOS, `versionName` on Android. - final String version; - - /// The build number. `CFBundleVersion` on iOS, `versionCode` on Android. - final String buildNumber; -} diff --git a/packages/package_info/macos/Assets/.gitkeep b/packages/package_info/macos/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h b/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h deleted file mode 100644 index 590e8263d951..000000000000 --- a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTPackageInfoPlugin : NSObject -@end diff --git a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.m b/packages/package_info/macos/Classes/FLTPackageInfoPlugin.m deleted file mode 120000 index a1ddabd76793..000000000000 --- a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.m +++ /dev/null @@ -1 +0,0 @@ -../../darwin/Classes/FLTPackageInfoPlugin.m \ No newline at end of file diff --git a/packages/package_info/macos/package_info.podspec b/packages/package_info/macos/package_info.podspec deleted file mode 100644 index dbe5bd9a105b..000000000000 --- a/packages/package_info/macos/package_info.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'package_info' - s.version = '0.0.1' - s.summary = 'Flutter plugin for querying information about the application package.' - s.description = <<-DESC -Flutter plugin for querying information about the application package, based on bundle data. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/package_info' - 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/package_info' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'FlutterMacOS' - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/packages/package_info/package_info_android.iml b/packages/package_info/package_info_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/package_info/package_info_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/package_info/pubspec.yaml b/packages/package_info/pubspec.yaml deleted file mode 100644 index dd9de6f14808..000000000000 --- a/packages/package_info/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: package_info -description: Flutter plugin for querying information about the application - package, such as CFBundleVersion on iOS or versionCode on Android. -repository: https://github.com/flutter/plugins/tree/master/packages/package_info -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+package_info%22 -version: 2.0.2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.packageinfo - pluginClass: PackageInfoPlugin - ios: - pluginClass: FLTPackageInfoPlugin - macos: - pluginClass: FLTPackageInfoPlugin - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/package_info/test/package_info_test.dart b/packages/package_info/test/package_info_test.dart deleted file mode 100644 index 91661de72103..000000000000 --- a/packages/package_info/test/package_info_test.dart +++ /dev/null @@ -1,49 +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:package_info/package_info.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const MethodChannel channel = - MethodChannel('plugins.flutter.io/package_info'); - late List log; - - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getAll': - return { - 'appName': 'package_info_example', - 'buildNumber': '1', - 'packageName': 'io.flutter.plugins.packageinfoexample', - 'version': '1.0', - }; - default: - assert(false); - return null; - } - }); - - setUp(() { - log = []; - }); - - test('fromPlatform', () async { - final PackageInfo info = await PackageInfo.fromPlatform(); - expect(info.appName, 'package_info_example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); - expect(info.version, '1.0'); - expect( - log, - [ - isMethodCall('getAll', arguments: null), - ], - ); - }); -} diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 764662d5d84b..6f345f8c3755 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,34 @@ +## 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. +* Updates example application. + +## 2.0.8 + +* Updates example app Android compileSdkVersion to 31. +* Removes obsolete manual registration of Windows and Linux implementations. + +## 2.0.7 + +* Moved Android and iOS implementations to federated packages. + +## 2.0.6 + +* Added support for Background Platform Channels on Android when it is + available. + ## 2.0.5 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 47ae6891d294..3a52e3e72050 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -2,16 +2,20 @@ [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) -A Flutter plugin for finding commonly used locations on the filesystem. Supports Android, iOS, Linux, macOS and Windows. +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). -### Example - -``` dart +## Example +```dart Directory tempDir = await getTemporaryDirectory(); String tempPath = tempDir.path; @@ -19,11 +23,24 @@ Directory appDocDir = await getApplicationDocumentsDirectory(); String appDocPath = appDocDir.path; ``` -Please see the example app of this plugin for a full example. +## Supported platforms and paths + +Directories support by platform: + +| Directory | Android | iOS | Linux | macOS | Windows | +| :--- | :---: | :---: | :---: | :---: | :---: | +| Temporary | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| Application Support | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| Application Library | ❌️ | ✔️ | ❌️ | ✔️ | ❌️ | +| Application Documents | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| External Storage | ✔️ | ❌ | ❌ | ❌️ | ❌️ | +| External Cache Directories | ✔️ | ❌ | ❌ | ❌️ | ❌️ | +| External Storage Directories | ✔️ | ❌ | ❌ | ❌️ | ❌️ | +| Downloads | ❌ | ❌ | ✔️ | ✔️ | ✔️ | -### Usage in tests +## Testing -`path_provider` now uses a `PlatformInterface`, meaning that not all platforms share the a single `PlatformChannel`-based implementation. +`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/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle deleted file mode 100644 index 1a22f135fe5a..000000000000 --- a/packages/path_provider/path_provider/android/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -group 'io.flutter.plugins.pathprovider' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - implementation 'androidx.annotation:annotation:1.1.0' - implementation 'com.google.guava:guava:28.1-android' - testImplementation 'junit:junit:4.12' -} diff --git a/packages/path_provider/path_provider/android/settings.gradle b/packages/path_provider/path_provider/android/settings.gradle deleted file mode 100644 index 71bc90768477..000000000000 --- a/packages/path_provider/path_provider/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'path_provider' diff --git a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java deleted file mode 100644 index 49360809e892..000000000000 --- a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ /dev/null @@ -1,183 +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.pathprovider; - -import android.content.Context; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.os.Handler; -import android.os.Looper; -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.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.util.PathUtils; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -public class PathProviderPlugin implements FlutterPlugin, MethodCallHandler { - - private Context context; - private MethodChannel channel; - 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 PathProviderPlugin() {} - - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - PathProviderPlugin instance = new PathProviderPlugin(); - instance.channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/path_provider"); - instance.context = registrar.context(); - instance.channel.setMethodCallHandler(instance); - } - - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - channel = new MethodChannel(binding.getBinaryMessenger(), "plugins.flutter.io/path_provider"); - context = binding.getApplicationContext(); - channel.setMethodCallHandler(this); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - channel.setMethodCallHandler(null); - channel = null; - } - - 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); - } - }); - } - - @Override - public void onMethodCall(MethodCall call, @NonNull Result result) { - switch (call.method) { - case "getTemporaryDirectory": - executeInBackground(() -> getPathProviderTemporaryDirectory(), result); - break; - case "getApplicationDocumentsDirectory": - executeInBackground(() -> getPathProviderApplicationDocumentsDirectory(), result); - break; - case "getStorageDirectory": - executeInBackground(() -> getPathProviderStorageDirectory(), result); - break; - case "getExternalCacheDirectories": - executeInBackground(() -> getPathProviderExternalCacheDirectories(), result); - break; - case "getExternalStorageDirectories": - final Integer type = call.argument("type"); - final String directoryName = StorageDirectoryMapper.androidType(type); - executeInBackground(() -> getPathProviderExternalStorageDirectories(directoryName), result); - break; - case "getApplicationSupportDirectory": - executeInBackground(() -> getApplicationSupportDirectory(), result); - break; - default: - result.notImplemented(); - } - } - - private String getPathProviderTemporaryDirectory() { - return context.getCacheDir().getPath(); - } - - private String getApplicationSupportDirectory() { - return PathUtils.getFilesDir(context); - } - - private String getPathProviderApplicationDocumentsDirectory() { - return PathUtils.getDataDirectory(context); - } - - private String getPathProviderStorageDirectory() { - final File dir = context.getExternalFilesDir(null); - if (dir == null) { - return null; - } - return dir.getAbsolutePath(); - } - - private List getPathProviderExternalCacheDirectories() { - final List paths = new ArrayList<>(); - - if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { - for (File dir : context.getExternalCacheDirs()) { - if (dir != null) { - paths.add(dir.getAbsolutePath()); - } - } - } else { - File dir = context.getExternalCacheDir(); - if (dir != null) { - paths.add(dir.getAbsolutePath()); - } - } - - return paths; - } - - private List getPathProviderExternalStorageDirectories(String type) { - final List paths = new ArrayList<>(); - - if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { - for (File dir : context.getExternalFilesDirs(type)) { - if (dir != null) { - paths.add(dir.getAbsolutePath()); - } - } - } else { - File dir = context.getExternalFilesDir(type); - if (dir != null) { - paths.add(dir.getAbsolutePath()); - } - } - - 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/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 e7f1bfb111a2..6d2bd6dadc36 100644 --- a/packages/path_provider/path_provider/example/android/app/build.gradle +++ b/packages/path_provider/path_provider/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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -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/integration_test/path_provider_test.dart b/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart index 9f8feee99ee2..6b3dd65fcb14 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 @@ -70,7 +70,8 @@ void main() { ]; for (final StorageDirectory? type in _allDirs) { - test('getExternalStorageDirectories (type: $type)', () async { + testWidgets('getExternalStorageDirectories (type: $type)', + (WidgetTester tester) async { if (Platform.isIOS) { final Future?> result = getExternalStorageDirectories(type: null); @@ -85,6 +86,23 @@ void main() { } }); } + + testWidgets('getDownloadsDirectory', (WidgetTester tester) async { + if (Platform.isIOS || Platform.isAndroid) { + final Future result = getDownloadsDirectory(); + expect(result, throwsA(isInstanceOf())); + } else { + final Directory? result = await getDownloadsDirectory(); + if (Platform.isMacOS) { + // On recent versions of macOS, actually using the downloads directory + // requires a user prompt, so will fail on CI. Instead, just check that + // it returned a path with the expected directory name. + expect(result?.path, endsWith('Downloads')); + } else { + _verifySampleFile(result, 'downloads'); + } + } + }); } /// Verify a file called [name] in [directory] by recreating it with test diff --git a/packages/path_provider/path_provider/example/ios/Runner/main.m b/packages/path_provider/path_provider/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/path_provider/path_provider/example/ios/Runner/main.m +++ b/packages/path_provider/path_provider/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/path_provider/path_provider/example/ios/RunnerTests/PathProviderTests.m b/packages/path_provider/path_provider/example/ios/RunnerTests/PathProviderTests.m index be48ea6b7ddf..0fcc05043c0a 100644 --- a/packages/path_provider/path_provider/example/ios/RunnerTests/PathProviderTests.m +++ b/packages/path_provider/path_provider/example/ios/RunnerTests/PathProviderTests.m @@ -11,7 +11,7 @@ @interface PathProviderTests : XCTestCase @implementation PathProviderTests - (void)testPlugin { - FLTPathProviderPlugin* plugin = [[FLTPathProviderPlugin alloc] init]; + FLTPathProviderPlugin *plugin = [[FLTPathProviderPlugin alloc] init]; XCTAssertNotNil(plugin); } diff --git a/packages/path_provider/path_provider/example/lib/main.dart b/packages/path_provider/path_provider/example/lib/main.dart index c0ac126b2a00..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 { @@ -42,6 +44,7 @@ class _MyHomePageState extends State { Future? _externalDocumentsDirectory; Future?>? _externalStorageDirectories; Future?>? _externalCacheDirectories; + Future? _downloadsDirectory; void _requestTempDirectory() { setState(() { @@ -117,6 +120,12 @@ class _MyHomePageState extends State { }); } + void _requestDownloadsDirectory() { + setState(() { + _downloadsDirectory = getDownloadsDirectory(); + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -126,88 +135,165 @@ class _MyHomePageState extends State { body: Center( child: ListView( children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: const Text('Get Temporary Directory'), - onPressed: _requestTempDirectory, - ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestTempDirectory, + child: const Text( + 'Get Temporary Directory', + ), + ), + ), + FutureBuilder( + future: _tempDirectory, + builder: _buildDirectory, + ), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppDocumentsDirectory, + child: const Text( + 'Get Application Documents Directory', + ), + ), + ), + FutureBuilder( + future: _appDocumentsDirectory, + builder: _buildDirectory, + ), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppSupportDirectory, + child: const Text( + 'Get Application Support Directory', + ), + ), + ), + FutureBuilder( + future: _appSupportDirectory, + builder: _buildDirectory, + ), + ], + ), + Column( + children: [ + 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', + ), + ), + ), + FutureBuilder( + future: _appLibraryDirectory, + builder: _buildDirectory, + ), + ], ), - FutureBuilder( - future: _tempDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: const Text('Get Application Documents Directory'), - onPressed: _requestAppDocumentsDirectory, - ), + Column( + children: [ + 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', + ), + ), + ), + FutureBuilder( + future: _externalDocumentsDirectory, + builder: _buildDirectory, + ), + ], ), - FutureBuilder( - future: _appDocumentsDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: const Text('Get Application Support Directory'), - onPressed: _requestAppSupportDirectory, - ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: !Platform.isAndroid + ? null + : () { + _requestExternalStorageDirectories( + StorageDirectory.music, + ); + }, + child: Text( + !Platform.isAndroid + ? 'External directories are unavailable' + : 'Get External Storage Directories', + ), + ), + ), + FutureBuilder?>( + future: _externalStorageDirectories, + builder: _buildDirectories, + ), + ], ), - FutureBuilder( - future: _appSupportDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: const Text('Get Application Library Directory'), - onPressed: _requestAppLibraryDirectory, - ), + Column( + children: [ + 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', + ), + ), + ), + FutureBuilder?>( + future: _externalCacheDirectories, + builder: _buildDirectories, + ), + ], ), - FutureBuilder( - future: _appLibraryDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: Text(Platform.isIOS - ? 'External directories are unavailable on iOS' - : 'Get External Storage Directory'), - onPressed: - Platform.isIOS ? null : _requestExternalStorageDirectory, - ), + Column( + children: [ + 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', + ), + ), + ), + FutureBuilder( + future: _downloadsDirectory, + builder: _buildDirectory, + ), + ], ), - FutureBuilder( - future: _externalDocumentsDirectory, builder: _buildDirectory), - Column(children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: Text(Platform.isIOS - ? 'External directories are unavailable on iOS' - : 'Get External Storage Directories'), - onPressed: Platform.isIOS - ? null - : () { - _requestExternalStorageDirectories( - StorageDirectory.music, - ); - }, - ), - ), - ]), - FutureBuilder?>( - future: _externalStorageDirectories, - builder: _buildDirectories), - Column(children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - child: Text(Platform.isIOS - ? 'External directories are unavailable on iOS' - : 'Get External Cache Directories'), - onPressed: - Platform.isIOS ? null : _requestExternalCacheDirectories, - ), - ), - ]), - FutureBuilder?>( - future: _externalCacheDirectories, builder: _buildDirectories), ], ), ), diff --git a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 0d56f519c923..000000000000 --- a/packages/path_provider/path_provider/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import path_provider_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) -} diff --git a/packages/path_provider/path_provider/example/macos/Runner/DebugProfile.entitlements b/packages/path_provider/path_provider/example/macos/Runner/DebugProfile.entitlements index dddb8a30c851..f83e1f42d120 100644 --- a/packages/path_provider/path_provider/example/macos/Runner/DebugProfile.entitlements +++ b/packages/path_provider/path_provider/example/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.files.downloads.read-write + diff --git a/packages/path_provider/path_provider/example/macos/Runner/Release.entitlements b/packages/path_provider/path_provider/example/macos/Runner/Release.entitlements index 852fa1a4728a..9d379927fbcb 100644 --- a/packages/path_provider/path_provider/example/macos/Runner/Release.entitlements +++ b/packages/path_provider/path_provider/example/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index 0001fe580e78..ea6f499622f9 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.8.0" dependencies: flutter: @@ -22,7 +22,6 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/path_provider/path_provider/example/windows/runner/main.cpp b/packages/path_provider/path_provider/example/windows/runner/main.cpp index 126302b0be18..c7dbde1c7123 100644 --- a/packages/path_provider/path_provider/example/windows/runner/main.cpp +++ b/packages/path_provider/path_provider/example/windows/runner/main.cpp @@ -11,7 +11,7 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _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()) { diff --git a/packages/path_provider/path_provider/example/windows/runner/utils.cpp b/packages/path_provider/path_provider/example/windows/runner/utils.cpp index 537728149601..e875ce8b05a9 100644 --- a/packages/path_provider/path_provider/example/windows/runner/utils.cpp +++ b/packages/path_provider/path_provider/example/windows/runner/utils.cpp @@ -13,7 +13,7 @@ void CreateAndAttachConsole() { if (::AllocConsole()) { - FILE *unused; + FILE* unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } diff --git a/packages/path_provider/path_provider/ios/Assets/.gitkeep b/packages/path_provider/path_provider/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m b/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m deleted file mode 100644 index 4d3dfeb6e6e6..000000000000 --- a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m +++ /dev/null @@ -1,69 +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 "FLTPathProviderPlugin.h" - -NSString* GetDirectoryOfType(NSSearchPathDirectory dir) { - NSArray* paths = NSSearchPathForDirectoriesInDomains(dir, NSUserDomainMask, YES); - return paths.firstObject; -} - -static FlutterError* getFlutterError(NSError* error) { - if (error == nil) return nil; - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", (long)error.code] - message:error.domain - details:error.localizedDescription]; -} - -@implementation FLTPathProviderPlugin - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/path_provider" - 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]) { - NSString* path = [self getApplicationSupportDirectory]; - - // Create the path if it doesn't exist - NSError* error; - NSFileManager* fileManager = [NSFileManager defaultManager]; - BOOL success = [fileManager createDirectoryAtPath:path - withIntermediateDirectories:YES - attributes:nil - error:&error]; - if (!success) { - result(getFlutterError(error)); - } else { - result(path); - } - } else if ([@"getLibraryDirectory" isEqualToString:call.method]) { - result([self getLibraryDirectory]); - } else { - result(FlutterMethodNotImplemented); - } - }]; -} - -+ (NSString*)getTemporaryDirectory { - return GetDirectoryOfType(NSCachesDirectory); -} - -+ (NSString*)getApplicationDocumentsDirectory { - return GetDirectoryOfType(NSDocumentDirectory); -} - -+ (NSString*)getApplicationSupportDirectory { - return GetDirectoryOfType(NSApplicationSupportDirectory); -} - -+ (NSString*)getLibraryDirectory { - return GetDirectoryOfType(NSLibraryDirectory); -} - -@end diff --git a/packages/path_provider/path_provider/ios/path_provider.podspec b/packages/path_provider/path_provider/ios/path_provider.podspec deleted file mode 100644 index 86f27c6c8fa5..000000000000 --- a/packages/path_provider/path_provider/ios/path_provider.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider' - s.version = '0.0.1' - s.summary = 'Flutter Path Provider' - s.description = <<-DESC -A Flutter plugin for getting commonly used locations on the filesystem. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider' } - s.documentation_url = 'https://pub.dev/packages/path_provider' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index e690b7f92960..e89d29dc0036 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -2,14 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io' show Directory, Platform; +import 'dart:io' show Directory; -import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; -import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; -// ignore: implementation_imports -import 'package:path_provider_platform_interface/src/method_channel_path_provider.dart'; -import 'package:path_provider_windows/path_provider_windows.dart'; export 'package:path_provider_platform_interface/path_provider_platform_interface.dart' show StorageDirectory; @@ -18,8 +14,6 @@ export 'package:path_provider_platform_interface/path_provider_platform_interfac @Deprecated('This is no longer necessary, and is now a no-op') set disablePathProviderPlatformOverride(bool override) {} -bool _manualDartRegistrationNeeded = true; - /// An exception thrown when a directory that should always be available on /// the current platform cannot be obtained. class MissingPlatformDirectoryException implements Exception { @@ -41,24 +35,7 @@ class MissingPlatformDirectoryException implements Exception { } } -PathProviderPlatform get _platform { - // TODO(egarciad): Remove once auto registration lands on Flutter stable. - // https://github.com/flutter/flutter/issues/81421. - if (_manualDartRegistrationNeeded) { - // Only do the initial registration if it hasn't already been overridden - // with a non-default instance. - if (!kIsWeb && PathProviderPlatform.instance is MethodChannelPathProvider) { - if (Platform.isLinux) { - PathProviderPlatform.instance = PathProviderLinux(); - } else if (Platform.isWindows) { - PathProviderPlatform.instance = PathProviderWindows(); - } - } - _manualDartRegistrationNeeded = false; - } - - return PathProviderPlatform.instance; -} +PathProviderPlatform get _platform => PathProviderPlatform.instance; /// Path to the temporary directory on the device that is not backed up and is /// suitable for storing caches of downloaded files. diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 5e9bc0b0e7c4..272cb44c5617 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -1,21 +1,20 @@ 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/master/packages/path_provider/path_provider +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.5 +version: 2.0.11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.pathprovider - pluginClass: PathProviderPlugin + default_package: path_provider_android ios: - pluginClass: FLTPathProviderPlugin + default_package: path_provider_ios macos: default_package: path_provider_macos linux: @@ -26,10 +25,12 @@ flutter: dependencies: flutter: sdk: flutter - path_provider_linux: ^2.0.0 + path_provider_android: ^2.0.6 + path_provider_ios: ^2.0.6 + path_provider_linux: ^2.0.1 path_provider_macos: ^2.0.0 path_provider_platform_interface: ^2.0.0 - path_provider_windows: ^2.0.0 + path_provider_windows: ^2.0.2 dev_dependencies: flutter_driver: @@ -38,6 +39,5 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 plugin_platform_interface: ^2.0.0 test: ^1.16.0 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/connectivity/connectivity_macos/AUTHORS b/packages/path_provider/path_provider_android/AUTHORS similarity index 100% rename from packages/connectivity/connectivity_macos/AUTHORS rename to packages/path_provider/path_provider_android/AUTHORS diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md new file mode 100644 index 000000000000..c40c103d5848 --- /dev/null +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -0,0 +1,48 @@ +## 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 + 2.0.11, but updates the minimum Flutter version to 2.8 to avoid the issue + that caused the revert. + +## 2.0.11 + +* Temporarily reverts the platform channel name change from 2.0.10 in order to + restore compatibility with Flutter versions earlier than 2.8. + +## 2.0.10 + +* Switches to a package-internal implementation of the platform interface. + +## 2.0.9 + +* Updates Android compileSdkVersion to 31. + +## 2.0.8 + +* Updates example app Android compileSdkVersion to 31. +* Fixes typing build warning. + +## 2.0.7 + +* Fixes link in README. + +## 2.0.6 + +* Split from `path_provider` as a federated implementation. diff --git a/packages/share/LICENSE b/packages/path_provider/path_provider_android/LICENSE similarity index 100% rename from packages/share/LICENSE rename to packages/path_provider/path_provider_android/LICENSE diff --git a/packages/path_provider/path_provider_android/README.md b/packages/path_provider/path_provider_android/README.md new file mode 100644 index 000000000000..b425b5eb5a9a --- /dev/null +++ b/packages/path_provider/path_provider_android/README.md @@ -0,0 +1,11 @@ +# path\_provider\_android + +The Android implementation of [`path_provider`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_android/android/build.gradle b/packages/path_provider/path_provider_android/android/build.gradle new file mode 100644 index 000000000000..4bfa738ac44c --- /dev/null +++ b/packages/path_provider/path_provider_android/android/build.gradle @@ -0,0 +1,58 @@ +group 'io.flutter.plugins.pathprovider' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation 'androidx.annotation:annotation:1.1.0' + implementation 'com.google.guava:guava:28.1-android' + testImplementation 'junit:junit:4.13.2' +} diff --git a/packages/path_provider/path_provider_android/android/settings.gradle b/packages/path_provider/path_provider_android/android/settings.gradle new file mode 100644 index 000000000000..359a57ff9540 --- /dev/null +++ b/packages/path_provider/path_provider_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'path_provider_android' diff --git a/packages/path_provider/path_provider/android/src/main/AndroidManifest.xml b/packages/path_provider/path_provider_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/path_provider/path_provider/android/src/main/AndroidManifest.xml rename to packages/path_provider/path_provider_android/android/src/main/AndroidManifest.xml 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 new file mode 100644 index 000000000000..7ef82198b22c --- /dev/null +++ b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -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. + +package io.flutter.plugins.pathprovider; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.Log; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; +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.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +public class PathProviderPlugin implements FlutterPlugin, PathProviderApi { + static final String TAG = "PathProviderPlugin"; + private Context context; + + public PathProviderPlugin() {} + + private void setup(BinaryMessenger messenger, Context context) { + TaskQueue taskQueue = messenger.makeBackgroundTaskQueue(); + + try { + PathProviderApi.setup(messenger, this); + } catch (Exception ex) { + Log.e(TAG, "Received exception while setting up PathProviderPlugin", ex); + } + + this.context = context; + } + + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + PathProviderPlugin instance = new PathProviderPlugin(); + instance.setup(registrar.messenger(), registrar.context()); + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + setup(binding.getBinaryMessenger(), binding.getApplicationContext()); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + PathProviderApi.setup(binding.getBinaryMessenger(), null); + } + + @Override + 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() { + return context.getCacheDir().getPath(); + } + + private String getApplicationSupportDirectory() { + return PathUtils.getFilesDir(context); + } + + private String getPathProviderApplicationDocumentsDirectory() { + return PathUtils.getDataDirectory(context); + } + + private String getPathProviderStorageDirectory() { + final File dir = context.getExternalFilesDir(null); + if (dir == null) { + return null; + } + return dir.getAbsolutePath(); + } + + private List getPathProviderExternalCacheDirectories() { + final List paths = new ArrayList(); + + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + for (File dir : context.getExternalCacheDirs()) { + if (dir != null) { + paths.add(dir.getAbsolutePath()); + } + } + } else { + File dir = context.getExternalCacheDir(); + if (dir != null) { + paths.add(dir.getAbsolutePath()); + } + } + + return paths; + } + + 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(getStorageDirectoryString(directory))) { + if (dir != null) { + paths.add(dir.getAbsolutePath()); + } + } + } else { + File dir = context.getExternalFilesDir(getStorageDirectoryString(directory)); + if (dir != null) { + paths.add(dir.getAbsolutePath()); + } + } + + return paths; + } +} diff --git a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java similarity index 100% rename from packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java rename to packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java diff --git a/packages/path_provider/path_provider/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java b/packages/path_provider/path_provider_android/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java similarity index 100% rename from packages/path_provider/path_provider/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java rename to packages/path_provider/path_provider_android/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java diff --git a/packages/path_provider/path_provider_android/example/README.md b/packages/path_provider/path_provider_android/example/README.md new file mode 100644 index 000000000000..801f44409938 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/README.md @@ -0,0 +1,3 @@ +# path_provider_example + +Demonstrates how to use the path_provider plugin. 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 new file mode 100644 index 000000000000..6d2bd6dadc36 --- /dev/null +++ b/packages/path_provider/path_provider_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.pathproviderexample" + minSdkVersion 16 + 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 + } + } +} + +flutter { + source '../..' +} + +dependencies { + 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.13.2' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/connectivity/connectivity/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/connectivity/connectivity/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java b/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java new file mode 100644 index 000000000000..d56458bd753c --- /dev/null +++ b/packages/path_provider/path_provider_android/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/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.pathprovider; + +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/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml b/packages/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..df8cee7bc3be --- /dev/null +++ b/packages/path_provider/path_provider_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/packages/local_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/local_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/local_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/local_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/local_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/local_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/local_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/local_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/local_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/local_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/path_provider/path_provider_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/android_intent/example/android/build.gradle b/packages/path_provider/path_provider_android/example/android/build.gradle similarity index 100% rename from packages/android_intent/example/android/build.gradle rename to packages/path_provider/path_provider_android/example/android/build.gradle diff --git a/packages/battery/battery/example/android/gradle.properties b/packages/path_provider/path_provider_android/example/android/gradle.properties similarity index 100% rename from packages/battery/battery/example/android/gradle.properties rename to packages/path_provider/path_provider_android/example/android/gradle.properties 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 new file mode 100644 index 000000000000..caf54fa2801c --- /dev/null +++ b/packages/path_provider/path_provider_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-5.1.1-all.zip diff --git a/packages/path_provider/path_provider_android/example/android/settings.gradle b/packages/path_provider/path_provider_android/example/android/settings.gradle new file mode 100644 index 000000000000..6cb349eef1b6 --- /dev/null +++ b/packages/path_provider/path_provider_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 +} \ No newline at end of file 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 new file mode 100644 index 000000000000..2be88130b4e7 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/integration_test/path_provider_test.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. + +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + expect(() => provider.getLibraryPath(), + throwsA(isInstanceOf())); + }); + + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getExternalStoragePath(); + _verifySampleFile(result, 'externalStorage'); + }); + + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final List? directories = await provider.getExternalCachePaths(); + expect(directories, isNotNull); + for (final String result in directories!) { + _verifySampleFile(result, 'externalCache'); + } + }); + + final List _allDirs = [ + null, + StorageDirectory.music, + StorageDirectory.podcasts, + StorageDirectory.ringtones, + StorageDirectory.alarms, + StorageDirectory.notifications, + StorageDirectory.pictures, + StorageDirectory.movies, + ]; + + 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'); + } + }); + } +} + +/// Verify a file called [name] in [directoryPath] by recreating it with test +/// contents when necessary. +void _verifySampleFile(String? directoryPath, String name) { + expect(directoryPath, isNotNull); + if (directoryPath == null) { + return; + } + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_android/example/lib/main.dart b/packages/path_provider/path_provider_android/example/lib/main.dart new file mode 100644 index 000000000000..fc9424a33542 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/lib/main.dart @@ -0,0 +1,191 @@ +// Copyright 2013 The Flutter 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:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Path Provider', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Path Provider'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final PathProviderPlatform provider = PathProviderPlatform.instance; + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appDocumentsDirectory; + Future? _externalDocumentsDirectory; + Future?>? _externalStorageDirectories; + Future?>? _externalCacheDirectories; + + void _requestTempDirectory() { + setState(() { + _tempDirectory = provider.getTemporaryPath(); + }); + } + + Widget _buildDirectory( + BuildContext context, AsyncSnapshot snapshot) { + Text text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + text = Text('path: ${snapshot.data}'); + } else { + text = const Text('path unavailable'); + } + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + Widget _buildDirectories( + BuildContext context, AsyncSnapshot?> snapshot) { + Text text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + final String combined = snapshot.data!.join(', '); + text = Text('paths: $combined'); + } else { + text = const Text('path unavailable'); + } + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + void _requestAppDocumentsDirectory() { + setState(() { + _appDocumentsDirectory = provider.getApplicationDocumentsPath(); + }); + } + + void _requestAppSupportDirectory() { + setState(() { + _appSupportDirectory = provider.getApplicationSupportPath(); + }); + } + + void _requestExternalStorageDirectory() { + setState(() { + _externalDocumentsDirectory = provider.getExternalStoragePath(); + }); + } + + void _requestExternalStorageDirectories(StorageDirectory type) { + setState(() { + _externalStorageDirectories = + provider.getExternalStoragePaths(type: type); + }); + } + + void _requestExternalCacheDirectories() { + setState(() { + _externalCacheDirectories = provider.getExternalCachePaths(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestTempDirectory, + child: const Text('Get Temporary Directory'), + ), + ), + FutureBuilder( + future: _tempDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppDocumentsDirectory, + child: const Text('Get Application Documents Directory'), + ), + ), + FutureBuilder( + future: _appDocumentsDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppSupportDirectory, + child: const Text('Get Application Support Directory'), + ), + ), + FutureBuilder( + future: _appSupportDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestExternalStorageDirectory, + child: const Text('Get External Storage Directory'), + ), + ), + FutureBuilder( + future: _externalDocumentsDirectory, builder: _buildDirectory), + Column(children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + child: const Text('Get External Storage Directories'), + onPressed: () { + _requestExternalStorageDirectories( + StorageDirectory.music, + ); + }, + ), + ), + ]), + FutureBuilder?>( + future: _externalStorageDirectories, + builder: _buildDirectories), + Column(children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestExternalCacheDirectories, + child: const Text('Get External Cache Directories'), + ), + ), + ]), + FutureBuilder?>( + future: _externalCacheDirectories, builder: _buildDirectories), + ], + ), + ), + ); + } +} diff --git a/packages/path_provider/path_provider_android/example/pubspec.yaml b/packages/path_provider/path_provider_android/example/pubspec.yaml new file mode 100644 index 000000000000..d546d9f2d729 --- /dev/null +++ b/packages/path_provider/path_provider_android/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + path_provider_android: + # When depending on this package from a real application you should use: + # path_provider: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/path_provider/path_provider_android/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_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/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 new file mode 100644 index 000000000000..f5c74f540253 --- /dev/null +++ b/packages/path_provider/path_provider_android/lib/path_provider_android.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: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 { + final messages.PathProviderApi _api = messages.PathProviderApi(); + + /// Registers this class as the default instance of [PathProviderPlatform]. + static void registerWith() { + PathProviderPlatform.instance = PathProviderAndroid(); + } + + @override + Future getTemporaryPath() { + return _api.getTemporaryPath(); + } + + @override + Future getApplicationSupportPath() { + return _api.getApplicationSupportPath(); + } + + @override + Future getLibraryPath() { + throw UnsupportedError('getLibraryPath is not supported on Android'); + } + + @override + Future getApplicationDocumentsPath() { + return _api.getApplicationDocumentsPath(); + } + + @override + Future getExternalStoragePath() { + return _api.getExternalStoragePath(); + } + + @override + Future?> getExternalCachePaths() async { + return (await _api.getExternalCachePaths()).cast(); + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + return (await _api.getExternalStoragePaths(_convertStorageDirectory(type))) + .cast(); + } + + @override + Future getDownloadsPath() { + throw UnsupportedError('getDownloadsPath is not supported on Android'); + } +} 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 new file mode 100644 index 000000000000..4c228bbb43bc --- /dev/null +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -0,0 +1,33 @@ +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.16 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: path_provider + platforms: + android: + package: io.flutter.plugins.pathprovider + pluginClass: PathProviderPlugin + dartPluginClass: PathProviderAndroid + +dependencies: + flutter: + sdk: flutter + path_provider_platform_interface: ^2.0.1 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + 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 new file mode 100644 index 000000000000..e3011474a2a3 --- /dev/null +++ b/packages/path_provider/path_provider_android/test/path_provider_android_test.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. + +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(); + + group('PathProviderAndroid', () { + late PathProviderAndroid pathProvider; + + setUp(() async { + pathProvider = PathProviderAndroid(); + TestPathProviderApi.setup(_Api()); + }); + + test('getTemporaryPath', () async { + final String? path = await pathProvider.getTemporaryPath(); + expect(path, kTemporaryPath); + }); + + test('getApplicationSupportPath', () async { + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, kApplicationSupportPath); + }); + + test('getLibraryPath fails', () async { + try { + await pathProvider.getLibraryPath(); + fail('should throw UnsupportedError'); + } catch (e) { + expect(e, isUnsupportedError); + } + }); + + test('getApplicationDocumentsPath', () async { + final String? path = await pathProvider.getApplicationDocumentsPath(); + expect(path, kApplicationDocumentsPath); + }); + + test('getExternalCachePaths succeeds', () async { + final List? result = await pathProvider.getExternalCachePaths(); + expect(result!.length, 1); + expect(result.first, kExternalCachePaths); + }); + + for (final StorageDirectory? type in [ + ...StorageDirectory.values + ]) { + test('getExternalStoragePaths (type: $type) android succeeds', () async { + final List? result = + await pathProvider.getExternalStoragePaths(type: type); + expect(result!.length, 1); + expect(result.first, kExternalStoragePaths); + }); + } // end of for-loop + + test('getDownloadsPath fails', () async { + try { + await pathProvider.getDownloadsPath(); + fail('should throw UnsupportedError'); + } catch (e) { + expect(e, isUnsupportedError); + } + }); + }); +} diff --git a/packages/connectivity/connectivity_platform_interface/AUTHORS b/packages/path_provider/path_provider_ios/AUTHORS similarity index 100% rename from packages/connectivity/connectivity_platform_interface/AUTHORS rename to packages/path_provider/path_provider_ios/AUTHORS diff --git a/packages/path_provider/path_provider_ios/CHANGELOG.md b/packages/path_provider/path_provider_ios/CHANGELOG.md new file mode 100644 index 000000000000..8569c1b35027 --- /dev/null +++ b/packages/path_provider/path_provider_ios/CHANGELOG.md @@ -0,0 +1,20 @@ +## 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. + +## 2.0.7 + +* Fixes link in README. + +## 2.0.6 + +* Split from `path_provider` as a federated implementation. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/LICENSE b/packages/path_provider/path_provider_ios/LICENSE similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/LICENSE rename to packages/path_provider/path_provider_ios/LICENSE diff --git a/packages/path_provider/path_provider_ios/README.md b/packages/path_provider/path_provider_ios/README.md new file mode 100644 index 000000000000..dcfa06dcdfb0 --- /dev/null +++ b/packages/path_provider/path_provider_ios/README.md @@ -0,0 +1,11 @@ +# path\_provider\_ios + +The iOS implementation of [`path_provider`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_ios/example/README.md b/packages/path_provider/path_provider_ios/example/README.md new file mode 100644 index 000000000000..801f44409938 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/README.md @@ -0,0 +1,3 @@ +# path_provider_example + +Demonstrates how to use the path_provider plugin. diff --git a/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart new file mode 100644 index 000000000000..5c6cf5a63579 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.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 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getLibraryPath(); + _verifySampleFile(result, 'library'); + }); + + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + expect(() => provider.getExternalStoragePath(), + throwsA(isInstanceOf())); + }); + + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + expect(() => provider.getExternalCachePaths(), + throwsA(isInstanceOf())); + }); +} + +/// Verify a file called [name] in [directoryPath] by recreating it with test +/// contents when necessary. +void _verifySampleFile(String? directoryPath, String name) { + expect(directoryPath, isNotNull); + if (directoryPath == null) { + return; + } + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig b/packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig b/packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/path_provider/path_provider_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/share/example/ios/Podfile b/packages/path_provider/path_provider_ios/example/ios/Podfile similarity index 100% rename from packages/share/example/ios/Podfile rename to packages/path_provider/path_provider_ios/example/ios/Podfile 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 new file mode 100644 index 000000000000..601985b46ae6 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,609 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 60774162343BF6F19B3D65CE /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */; }; + 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0EE60090AA5F3AAAF2175B6 /* 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 */; }; + F76AC1DF26671E960040C8BC /* PathProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1DE26671E960040C8BC /* PathProviderTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F76AC1E126671E960040C8BC /* 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 */ + 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 29F2567B3AE74A9113ED3394 /* 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 = ""; }; + 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 6EB685EA3DDA2EED39600D11 /* 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 = ""; }; + 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 = ""; }; + C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F76AC1DC26671E960040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1DE26671E960040C8BC /* PathProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PathProviderTests.m; sourceTree = ""; }; + F76AC1E026671E960040C8BC /* 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 = ( + 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1D926671E960040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 60774162343BF6F19B3D65CE /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */, + D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */, + 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */, + 29F2567B3AE74A9113ED3394 /* 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 = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F76AC1DD26671E960040C8BC /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1DC26671E960040C8BC /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */, + 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */, + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */, + 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F76AC1DD26671E960040C8BC /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F76AC1DE26671E960040C8BC /* PathProviderTests.m */, + F76AC1E026671E960040C8BC /* Info.plist */, + ); + path = RunnerTests; + 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 */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F76AC1DB26671E960040C8BC /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1E526671E960040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 31566AD39C1C7EF9EB261E6F /* [CP] Check Pods Manifest.lock */, + F76AC1D826671E960040C8BC /* Sources */, + F76AC1D926671E960040C8BC /* Frameworks */, + F76AC1DA26671E960040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1E226671E960040C8BC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F76AC1DC26671E960040C8BC /* 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 = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F76AC1DB26671E960040C8BC = { + 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 */, + F76AC1DB26671E960040C8BC /* 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; + }; + F76AC1DA26671E960040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 31566AD39C1C7EF9EB261E6F /* [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_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; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC1D826671E960040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1DF26671E960040C8BC /* PathProviderTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F76AC1E226671E960040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1E126671E960040C8BC /* 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; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F76AC1E326671E960040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6EB685EA3DDA2EED39600D11 /* 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; + }; + F76AC1E426671E960040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 29F2567B3AE74A9113ED3394 /* 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; + }; +/* 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; + }; + F76AC1E526671E960040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1E326671E960040C8BC /* Debug */, + F76AC1E426671E960040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..ec3713b95db5 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/package_info/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/package_info/example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/sensors/example/ios/Runner/AppDelegate.h b/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/sensors/example/ios/Runner/AppDelegate.h rename to packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h diff --git a/packages/share/example/ios/Runner/AppDelegate.m b/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/share/example/ios/Runner/AppDelegate.m rename to packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m diff --git a/packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/package_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/package_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/package_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/sensors/example/ios/Runner/Base.lproj/Main.storyboard b/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/sensors/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..342db6a5dcaf --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + path_provider_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/main.m b/packages/path_provider/path_provider_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/path_provider/path_provider_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/shared_preferences/shared_preferences/example/ios/RunnerTests/Info.plist b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/shared_preferences/shared_preferences/example/ios/RunnerTests/Info.plist rename to packages/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m new file mode 100644 index 000000000000..87d227795614 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m @@ -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 path_provider_ios; +@import XCTest; + +@interface PathProviderTests : XCTestCase +@end + +@implementation PathProviderTests + +- (void)testPlugin { + FLTPathProviderPlugin *plugin = [[FLTPathProviderPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/path_provider/path_provider_ios/example/lib/main.dart b/packages/path_provider/path_provider_ios/example/lib/main.dart new file mode 100644 index 000000000000..d7140b76a06b --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/lib/main.dart @@ -0,0 +1,133 @@ +// Copyright 2013 The Flutter 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:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Path Provider', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Path Provider'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final PathProviderPlatform provider = PathProviderPlatform.instance; + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appLibraryDirectory; + Future? _appDocumentsDirectory; + + void _requestTempDirectory() { + setState(() { + _tempDirectory = provider.getTemporaryPath(); + }); + } + + Widget _buildDirectory( + BuildContext context, AsyncSnapshot snapshot) { + Text text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + text = Text('path: ${snapshot.data}'); + } else { + text = const Text('path unavailable'); + } + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + void _requestAppDocumentsDirectory() { + setState(() { + _appDocumentsDirectory = provider.getApplicationDocumentsPath(); + }); + } + + void _requestAppSupportDirectory() { + setState(() { + _appSupportDirectory = provider.getApplicationSupportPath(); + }); + } + + void _requestAppLibraryDirectory() { + setState(() { + _appLibraryDirectory = provider.getLibraryPath(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestTempDirectory, + child: const Text('Get Temporary Directory'), + ), + ), + FutureBuilder( + future: _tempDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppDocumentsDirectory, + child: const Text('Get Application Documents Directory'), + ), + ), + FutureBuilder( + future: _appDocumentsDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppSupportDirectory, + child: const Text('Get Application Support Directory'), + ), + ), + FutureBuilder( + future: _appSupportDirectory, builder: _buildDirectory), + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppLibraryDirectory, + child: const Text('Get Application Library Directory'), + ), + ), + FutureBuilder( + future: _appLibraryDirectory, builder: _buildDirectory), + ], + ), + ), + ); + } +} diff --git a/packages/path_provider/path_provider_ios/example/pubspec.yaml b/packages/path_provider/path_provider_ios/example/pubspec.yaml new file mode 100644 index 000000000000..00ac1f1af3a7 --- /dev/null +++ b/packages/path_provider/path_provider_ios/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + path_provider_ios: + # When depending on this package from a real application you should use: + # path_provider_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: ../ + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep rename to packages/path_provider/path_provider_ios/ios/Assets/.gitkeep diff --git a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h similarity index 100% rename from packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h rename to packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h diff --git a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m new file mode 100644 index 000000000000..82b8df5382fb --- /dev/null +++ b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.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 "FLTPathProviderPlugin.h" +#import "messages.g.h" + +static NSString *GetDirectoryOfType(NSSearchPathDirectory dir) { + NSArray *paths = NSSearchPathForDirectoriesInDomains(dir, NSUserDomainMask, YES); + return paths.firstObject; +} + +@interface FLTPathProviderPlugin () +@end + +@implementation FLTPathProviderPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTPathProviderPlugin *plugin = [[FLTPathProviderPlugin alloc] init]; + FLTPathProviderApiSetup(registrar.messenger, plugin); +} + +- (nullable NSString *)getApplicationDocumentsPathWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return GetDirectoryOfType(NSDocumentDirectory); +} + +- (nullable NSString *)getApplicationSupportPathWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return GetDirectoryOfType(NSApplicationSupportDirectory); +} + +- (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/ios/path_provider_ios.podspec b/packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec new file mode 100644 index 000000000000..f1f94e996093 --- /dev/null +++ b/packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'path_provider_ios' + s.version = '0.0.1' + s.summary = 'Flutter Path Provider' + s.description = <<-DESC +A Flutter plugin for getting commonly used locations on the filesystem. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_ios' } + s.documentation_url = 'https://pub.dev/packages/path_provider' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end 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 new file mode 100644 index 000000000000..05be0534764a --- /dev/null +++ b/packages/path_provider/path_provider_ios/lib/path_provider_ios.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. + +import 'dart:io'; + +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. + final PathProviderApi _pathProvider = PathProviderApi(); + + /// Registers this class as the default instance of [PathProviderPlatform] + static void registerWith() { + PathProviderPlatform.instance = PathProviderIOS(); + } + + @override + Future getTemporaryPath() async { + return _pathProvider.getTemporaryPath(); + } + + @override + Future getApplicationSupportPath() async { + final String? path = await _pathProvider.getApplicationSupportPath(); + if (path != null) { + // Ensure the directory exists before returning it, for consistency with + // other platforms. + await Directory(path).create(recursive: true); + } + return path; + } + + @override + Future getLibraryPath() async { + return _pathProvider.getLibraryPath(); + } + + @override + Future getApplicationDocumentsPath() async { + return _pathProvider.getApplicationDocumentsPath(); + } + + @override + Future getExternalStoragePath() async { + throw UnsupportedError('getExternalStoragePath is not supported on iOS'); + } + + @override + Future?> getExternalCachePaths() async { + throw UnsupportedError('getExternalCachePaths is not supported on iOS'); + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + throw UnsupportedError('getExternalStoragePaths is not supported on iOS'); + } + + @override + Future getDownloadsPath() async { + throw UnsupportedError('getDownloadsPath is not supported on iOS'); + } +} 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 new file mode 100644 index 000000000000..16d2f2e9021e --- /dev/null +++ b/packages/path_provider/path_provider_ios/pubspec.yaml @@ -0,0 +1,34 @@ +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.10 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" + +flutter: + plugin: + implements: path_provider + platforms: + ios: + pluginClass: FLTPathProviderPlugin + dartPluginClass: PathProviderIOS + +dependencies: + flutter: + sdk: flutter + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + 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 new file mode 100644 index 000000000000..16a7cd8d71a2 --- /dev/null +++ b/packages/path_provider/path_provider_ios/test/path_provider_ios_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:io'; + +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; + // 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. + late Directory testRoot; + + late String temporaryPath; + late String applicationSupportPath; + late String libraryPath; + late String applicationDocumentsPath; + late _Api api; + + setUp(() async { + pathProvider = PathProviderIOS(); + + testRoot = Directory.systemTemp.createTempSync(); + final String basePath = testRoot.path; + temporaryPath = p.join(basePath, 'temporary', 'path'); + applicationSupportPath = + p.join(basePath, 'application', 'support', 'path'); + libraryPath = p.join(basePath, 'library', 'path'); + applicationDocumentsPath = + p.join(basePath, 'application', 'documents', 'path'); + + api = _Api(); + api.applicationDocumentsPath = applicationDocumentsPath; + api.applicationSupportPath = applicationSupportPath; + api.libraryPath = libraryPath; + api.temporaryPath = temporaryPath; + TestPathProviderApi.setup(api); + }); + + tearDown(() { + testRoot.deleteSync(recursive: true); + }); + + test('getTemporaryPath', () async { + final String? path = await pathProvider.getTemporaryPath(); + expect(path, temporaryPath); + }); + + test('getApplicationSupportPath', () async { + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, applicationSupportPath); + }); + + test('getApplicationSupportPath creates the directory if necessary', + () async { + final String? path = await pathProvider.getApplicationSupportPath(); + expect(Directory(path!).existsSync(), isTrue); + }); + + test('getLibraryPath', () async { + final String? path = await pathProvider.getLibraryPath(); + expect(path, libraryPath); + }); + + test('getApplicationDocumentsPath', () async { + final String? path = await pathProvider.getApplicationDocumentsPath(); + expect(path, applicationDocumentsPath); + }); + + test('getDownloadsPath throws', () async { + expect(pathProvider.getDownloadsPath(), throwsA(isUnsupportedError)); + }); + + test('getExternalCachePaths throws', () async { + expect(pathProvider.getExternalCachePaths(), throwsA(isUnsupportedError)); + }); + + test('getExternalStoragePath throws', () async { + expect( + pathProvider.getExternalStoragePath(), throwsA(isUnsupportedError)); + }); + + test('getExternalStoragePaths throws', () async { + expect( + pathProvider.getExternalStoragePaths(), throwsA(isUnsupportedError)); + }); + }); +} diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 6f18d0d6ae58..b69ec900d614 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,34 @@ +## 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`. + +## 2.1.4 + +* Fixes `getApplicationSupportPath` handling of applications where the + application ID is not set. + +## 2.1.3 + +* Change getApplicationSupportPath from using executable name to application ID (if provided). + * If the executable name based directory exists, continue to use that so existing applications continue with the same behaviour. + +## 2.1.2 + +* Fixes link in README. + +## 2.1.1 + +* Removed obsolete `pluginClass: none` from pubpsec. + ## 2.1.0 * Now `getTemporaryPath` returns the value of the `TMPDIR` environment variable primarily. If `TMPDIR` is not set, `/tmp` is returned. @@ -27,20 +58,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 + +* The initial implementation of path\_provider for Linux * Implements getApplicationSupportPath, getApplicationDocumentsPath, getDownloadsPath, and getTemporaryPath diff --git a/packages/path_provider/path_provider_linux/README.md b/packages/path_provider/path_provider_linux/README.md index b0b73dcb0ecd..281873bdade1 100644 --- a/packages/path_provider/path_provider_linux/README.md +++ b/packages/path_provider/path_provider_linux/README.md @@ -1,6 +1,6 @@ # path\_provider\_linux -The linux implementation of [`path_provider`]. +The linux implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_linux/example/README.md b/packages/path_provider/path_provider_linux/example/README.md index 751fe4b811f0..333d0f55cec7 100644 --- a/packages/path_provider/path_provider_linux/example/README.md +++ b/packages/path_provider/path_provider_linux/example/README.md @@ -1,16 +1,3 @@ # path_provider_linux_example Demonstrates how to use the path_provider_linux 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/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_plugin_registrant.cc b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 252f3510a789..47ed4be220a6 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.8.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart index ab18db69ddfb..e32af1bf5f13 100644 --- a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart +++ b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart @@ -2,61 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; -import 'package:xdg_directories/xdg_directories.dart' as xdg; - -/// The linux implementation of [PathProviderPlatform] -/// -/// This class implements the `package:path_provider` functionality for linux -class PathProviderLinux extends PathProviderPlatform { - /// Constructs an instance of [PathProviderLinux] - PathProviderLinux() : _environment = Platform.environment; - - /// Constructs an instance of [PathProviderLinux] with the given [environment] - @visibleForTesting - PathProviderLinux.private({ - required Map environment, - }) : _environment = environment; - - final Map _environment; - - /// Registers this class as the default instance of [PathProviderPlatform] - static void registerWith() { - PathProviderPlatform.instance = PathProviderLinux(); - } - - @override - Future getTemporaryPath() { - final String environmentTmpDir = _environment['TMPDIR'] ?? ''; - return Future.value( - environmentTmpDir.isEmpty ? '/tmp' : environmentTmpDir, - ); - } - - @override - Future getApplicationSupportPath() async { - final String processName = path.basenameWithoutExtension( - await File('/proc/self/exe').resolveSymbolicLinks()); - final Directory directory = - Directory(path.join(xdg.dataHome.path, processName)); - // Creating the directory if it doesn't exist, because mobile implementations assume the directory exists - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - return directory.path; - } - - @override - Future getApplicationDocumentsPath() { - return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); - } - - @override - Future getDownloadsPath() { - return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); - } -} +export 'src/path_provider_linux.dart'; diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id.dart new file mode 100644 index 000000000000..e169c025eef1 --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id.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. + +// getApplicationId() is implemented using FFI; export a stub for platforms +// that don't support FFI (e.g., web) to avoid having transitive dependencies +// break web compilation. +export 'get_application_id_stub.dart' + if (dart.library.ffi) 'get_application_id_real.dart'; diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.dart new file mode 100644 index 000000000000..f01c3e4ee15e --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id_real.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:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; + +// GApplication* g_application_get_default(); +typedef _GApplicationGetDefaultC = IntPtr Function(); +typedef _GApplicationGetDefaultDart = int Function(); + +// const gchar* g_application_get_application_id(GApplication* application); +typedef _GApplicationGetApplicationIdC = Pointer Function(IntPtr); +typedef _GApplicationGetApplicationIdDart = Pointer Function(int); + +/// Interface for interacting with libgio. +@visibleForTesting +class GioUtils { + /// Creates a default instance that uses the real libgio. + GioUtils() { + try { + _gio = DynamicLibrary.open('libgio-2.0.so'); + } on ArgumentError { + _gio = null; + } + } + + DynamicLibrary? _gio; + + /// True if libgio was opened successfully. + bool get libraryIsPresent => _gio != null; + + /// Wraps `g_application_get_default`. + int gApplicationGetDefault() { + if (_gio == null) { + return 0; + } + final _GApplicationGetDefaultDart getDefault = _gio! + .lookupFunction<_GApplicationGetDefaultC, _GApplicationGetDefaultDart>( + 'g_application_get_default'); + return getDefault(); + } + + /// Wraps g_application_get_application_id. + Pointer gApplicationGetApplicationId(int app) { + if (_gio == null) { + return nullptr; + } + final _GApplicationGetApplicationIdDart gApplicationGetApplicationId = _gio! + .lookupFunction<_GApplicationGetApplicationIdC, + _GApplicationGetApplicationIdDart>( + 'g_application_get_application_id'); + return gApplicationGetApplicationId(app); + } +} + +/// Allows overriding the default GioUtils instance with a fake for testing. +@visibleForTesting +GioUtils? gioUtilsOverride; + +/// Gets the application ID for this app. +String? getApplicationId() { + final GioUtils gio = gioUtilsOverride ?? GioUtils(); + if (!gio.libraryIsPresent) { + return null; + } + + final int app = gio.gApplicationGetDefault(); + if (app == 0) { + return null; + } + final Pointer appId = gio.gApplicationGetApplicationId(app); + if (appId == null || appId == nullptr) { + return null; + } + return appId.toDartString(); +} diff --git a/packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart b/packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart new file mode 100644 index 000000000000..909997693626 --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/get_application_id_stub.dart @@ -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. + +/// Gets the application ID for this app. +String? getApplicationId() => null; diff --git a/packages/path_provider/path_provider_linux/lib/src/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/src/path_provider_linux.dart new file mode 100644 index 000000000000..1544dcea2984 --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/src/path_provider_linux.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 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg; + +import 'get_application_id.dart'; + +/// The linux implementation of [PathProviderPlatform] +/// +/// This class implements the `package:path_provider` functionality for Linux. +class PathProviderLinux extends PathProviderPlatform { + /// Constructs an instance of [PathProviderLinux] + PathProviderLinux() : _environment = Platform.environment; + + /// Constructs an instance of [PathProviderLinux] with the given [environment] + @visibleForTesting + PathProviderLinux.private( + {Map environment = const {}, + String? executableName, + String? applicationId}) + : _environment = environment, + _executableName = executableName, + _applicationId = applicationId; + + final Map _environment; + String? _executableName; + String? _applicationId; + + /// Registers this class as the default instance of [PathProviderPlatform] + static void registerWith() { + PathProviderPlatform.instance = PathProviderLinux(); + } + + @override + Future getTemporaryPath() { + final String environmentTmpDir = _environment['TMPDIR'] ?? ''; + return Future.value( + environmentTmpDir.isEmpty ? '/tmp' : environmentTmpDir, + ); + } + + @override + Future getApplicationSupportPath() async { + final Directory directory = + Directory(path.join(xdg.dataHome.path, await _getId())); + if (directory.existsSync()) { + return directory.path; + } + + // This plugin originally used the executable name as a directory. + // Use that if it exists for backwards compatibility. + final Directory legacyDirectory = + Directory(path.join(xdg.dataHome.path, await _getExecutableName())); + if (legacyDirectory.existsSync()) { + return legacyDirectory.path; + } + + // Create the directory, because mobile implementations assume the directory exists. + await directory.create(recursive: true); + return directory.path; + } + + @override + Future getApplicationDocumentsPath() { + return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); + } + + @override + Future getDownloadsPath() { + return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); + } + + // Gets the name of this executable. + Future _getExecutableName() async { + _executableName ??= path.basenameWithoutExtension( + await File('/proc/self/exe').resolveSymbolicLinks()); + return _executableName!; + } + + // Gets the unique ID for this application. + Future _getId() async { + _applicationId ??= getApplicationId(); + // If no application ID then fall back to using the executable name. + return _applicationId ?? await _getExecutableName(); + } +} diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index f5b7a88ca232..c0b7954087f5 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -1,12 +1,12 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin -repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux +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.0 +version: 2.1.7 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,9 +14,9 @@ flutter: platforms: linux: dartPluginClass: PathProviderLinux - pluginClass: none dependencies: + ffi: ">=1.1.2 <3.0.0" flutter: sdk: flutter path: ^1.8.0 @@ -26,4 +26,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_linux/test/get_application_id_test.dart b/packages/path_provider/path_provider_linux/test/get_application_id_test.dart new file mode 100644 index 000000000000..d9eb5163b5fe --- /dev/null +++ b/packages/path_provider/path_provider_linux/test/get_application_id_test.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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_linux/src/get_application_id_real.dart'; + +class _FakeGioUtils implements GioUtils { + int? application; + Pointer? applicationId; + + @override + bool libraryIsPresent = false; + + @override + int gApplicationGetDefault() => application!; + + @override + Pointer gApplicationGetApplicationId(int app) => applicationId!; +} + +void main() { + late _FakeGioUtils fakeGio; + + setUp(() { + fakeGio = _FakeGioUtils(); + gioUtilsOverride = fakeGio; + }); + + tearDown(() { + gioUtilsOverride = null; + }); + + test('returns null if libgio is not available', () { + expect(getApplicationId(), null); + }); + + test('returns null if g_paplication_get_default returns 0', () { + fakeGio.libraryIsPresent = true; + fakeGio.application = 0; + expect(getApplicationId(), null); + }); + + test('returns null if g_application_get_application_id returns nullptr', () { + fakeGio.libraryIsPresent = true; + fakeGio.application = 1; + fakeGio.applicationId = nullptr; + expect(getApplicationId(), null); + }); + + test('returns value if g_application_get_application_id returns a value', () { + fakeGio.libraryIsPresent = true; + fakeGio.application = 1; + const String id = 'foo'; + final Pointer idPtr = id.toNativeUtf8(); + fakeGio.applicationId = idPtr; + expect(getApplicationId(), id); + calloc.free(idPtr); + }); +} diff --git a/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart index 6dd35000a8ea..1f567c00513d 100644 --- a/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart +++ b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_linux/path_provider_linux.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -35,8 +36,20 @@ void main() { }); test('getApplicationSupportPath', () async { - final PathProviderPlatform plugin = PathProviderPlatform.instance; - expect(await plugin.getApplicationSupportPath(), startsWith('/')); + final PathProviderPlatform plugin = PathProviderLinux.private( + executableName: 'path_provider_linux_test_binary', + applicationId: 'com.example.Test'); + // Note this will fail if ${xdg.dataHome.path}/path_provider_linux_test_binary exists on the local filesystem. + expect(await plugin.getApplicationSupportPath(), + '${xdg.dataHome.path}/com.example.Test'); + }); + + test('getApplicationSupportPath uses executable name if no application Id', + () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + executableName: 'path_provider_linux_test_binary'); + expect(await plugin.getApplicationSupportPath(), + '${xdg.dataHome.path}/path_provider_linux_test_binary'); }); test('getApplicationDocumentsPath', () async { diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index 1d0738c3757a..c59ba971d461 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,4 +1,21 @@ -# 2.0.2 +## 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`. + +## 2.0.4 + +* Switches to a package-internal implementation of the platform interface. + +## 2.0.3 + +* Fixes link in README. + +## 2.0.2 * Add Swift language version to podspec. * Add native unit tests. @@ -55,7 +72,7 @@ ## 0.0.3+1 -* Make the pedantic dev_dependency explicit. +* Make the pedantic `dev_dependency` explicit. ## 0.0.3 diff --git a/packages/path_provider/path_provider_macos/README.md b/packages/path_provider/path_provider_macos/README.md index 00abdf24cd79..6641134aefd9 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_macos/README.md @@ -1,6 +1,6 @@ # path\_provider\_macos -The macos implementation of [`path_provider`]. +The macos implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_macos/example/README.md b/packages/path_provider/path_provider_macos/example/README.md index 4f413873b346..158869595c01 100644 --- a/packages/path_provider/path_provider_macos/example/README.md +++ b/packages/path_provider/path_provider_macos/example/README.md @@ -1,8 +1,3 @@ # path_provider_macos_example Demonstrates how to use the path_provider_macos plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). 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..42ed28b818d6 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.8.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart b/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart new file mode 100644 index 000000000000..5dc3176e9b89 --- /dev/null +++ b/packages/path_provider/path_provider_macos/lib/path_provider_macos.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 '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'; + +/// The macOS implementation of [PathProviderPlatform]. +class PathProviderMacOS extends PathProviderPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + MethodChannel methodChannel = + const MethodChannel('plugins.flutter.io/path_provider_macos'); + + /// Registers this class as the default instance of [PathProviderPlatform] + static void registerWith() { + PathProviderPlatform.instance = PathProviderMacOS(); + } + + @override + Future getTemporaryPath() { + return methodChannel.invokeMethod('getTemporaryDirectory'); + } + + @override + Future getApplicationSupportPath() async { + final String? path = await methodChannel + .invokeMethod('getApplicationSupportDirectory'); + if (path != null) { + // Ensure the directory exists before returning it, for consistency with + // other platforms. + await Directory(path).create(recursive: true); + } + return path; + } + + @override + Future getLibraryPath() { + return methodChannel.invokeMethod('getLibraryDirectory'); + } + + @override + Future getApplicationDocumentsPath() { + return methodChannel + .invokeMethod('getApplicationDocumentsDirectory'); + } + + @override + Future getExternalStoragePath() async { + throw UnsupportedError('getExternalStoragePath is not supported on macOS'); + } + + @override + Future?> getExternalCachePaths() async { + throw UnsupportedError('getExternalCachePaths is not supported on macOS'); + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + throw UnsupportedError('getExternalStoragePaths is not supported on macOS'); + } + + @override + Future getDownloadsPath() { + return methodChannel.invokeMethod('getDownloadsDirectory'); + } +} diff --git a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift index b308793be355..e138eee759ac 100644 --- a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift +++ b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift @@ -8,7 +8,7 @@ import Foundation public class PathProviderPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( - name: "plugins.flutter.io/path_provider", + name: "plugins.flutter.io/path_provider_macos", binaryMessenger: registrar.messenger) let instance = PathProviderPlugin() registrar.addMethodCallDelegate(instance, channel: channel) @@ -25,16 +25,6 @@ public class PathProviderPlugin: NSObject, FlutterPlugin { if let basePath = path { let basePathURL = URL.init(fileURLWithPath: basePath) path = basePathURL.appendingPathComponent(Bundle.main.bundleIdentifier!).path - do { - try FileManager.default.createDirectory(atPath: path!, withIntermediateDirectories: true) - } catch { - result( - FlutterError( - code: "directory_creation_failure", - message: error.localizedDescription, - details: "\(error)")) - return - } } result(path) case "getLibraryDirectory": 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 66b5872c9ac9..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 @@ -8,10 +8,10 @@ Pod::Spec.new do |s| s.description = <<-DESC A macOS implementation of the Flutter plugin for getting commonly used locations on the filesystem. DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos' + 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 140e4cde9d58..4381041079b5 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -1,12 +1,12 @@ name: path_provider_macos description: macOS implementation of the path_provider plugin -repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos +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.2 +version: 2.0.6 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,10 +14,14 @@ flutter: platforms: macos: pluginClass: PathProviderPlugin + dartPluginClass: PathProviderMacOS dependencies: flutter: sdk: flutter + path_provider_platform_interface: ^2.0.1 dev_dependencies: - pedantic: ^1.10.0 + flutter_test: + sdk: flutter + path: ^1.8.0 diff --git a/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart b/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart new file mode 100644 index 000000000000..7e783aad24e9 --- /dev/null +++ b/packages/path_provider/path_provider_macos/test/path_provider_macos_test.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:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider_macos/path_provider_macos.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('PathProviderMacOS', () { + late PathProviderMacOS 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. + late Directory testRoot; + + late String temporaryPath; + late String applicationSupportPath; + late String libraryPath; + late String applicationDocumentsPath; + late String downloadsPath; + + setUp(() async { + pathProvider = PathProviderMacOS(); + + testRoot = Directory.systemTemp.createTempSync(); + final String basePath = testRoot.path; + temporaryPath = p.join(basePath, 'temporary', 'path'); + applicationSupportPath = + p.join(basePath, 'application', 'support', 'path'); + libraryPath = p.join(basePath, 'library', 'path'); + applicationDocumentsPath = + p.join(basePath, 'application', 'documents', 'path'); + downloadsPath = p.join(basePath, 'downloads', '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; + case 'getDownloadsDirectory': + return downloadsPath; + default: + return null; + } + }); + }); + + tearDown(() { + testRoot.deleteSync(recursive: true); + }); + + 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); + }); + + test('getApplicationSupportPath creates the directory if necessary', + () async { + final String? path = await pathProvider.getApplicationSupportPath(); + expect(Directory(path!).existsSync(), isTrue); + }); + + 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); + }); + + test('getDownloadsPath', () async { + final String? result = await pathProvider.getDownloadsPath(); + expect( + log, + [isMethodCall('getDownloadsDirectory', arguments: null)], + ); + expect(result, downloadsPath); + }); + + test('getExternalCachePaths throws', () async { + expect(pathProvider.getExternalCachePaths(), throwsA(isUnsupportedError)); + }); + + test('getExternalStoragePath throws', () async { + expect( + pathProvider.getExternalStoragePath(), throwsA(isUnsupportedError)); + }); + + test('getExternalStoragePaths throws', () async { + expect( + pathProvider.getExternalStoragePaths(), throwsA(isUnsupportedError)); + }); + }); +} diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index eec0fe3866b5..4eea4b36ba8a 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,16 @@ +## 2.0.4 + +* Minor fixes for new analysis options. +* Removes unnecessary imports. + +## 2.0.3 + +* Removes dependency on `meta`. + +## 2.0.2 + +* Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. + ## 2.0.1 * Update platform_plugin_interface version requirement. diff --git a/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart index 99e600d05263..517ac74d8fa0 100644 --- a/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart +++ b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart @@ -32,7 +32,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [PathProviderPlatform] when they register themselves. static set instance(PathProviderPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } 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 007787444adb..fe632743b098 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 @@ -2,13 +2,11 @@ // 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 visibleForTesting; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:platform/platform.dart'; -import 'enums.dart'; - /// An implementation of [PathProviderPlatform] that uses method channels. class MethodChannelPathProvider extends PathProviderPlatform { /// The method channel used to interact with the native platform. @@ -24,6 +22,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 7fe5e8dfc232..92ec432dc394 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -1,23 +1,21 @@ name: path_provider_platform_interface description: A common platform interface for the path_provider plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_platform_interface 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.1 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - meta: ^1.3.0 platform: ^3.0.0 - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index 953bb894de09..3967c38be411 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,22 @@ +## 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`. + +## 2.0.4 + +* Removed obsolete `pluginClass: none` from pubpsec. + ## 2.0.3 * Updated installation instructions in README. diff --git a/packages/path_provider/path_provider_windows/example/README.md b/packages/path_provider/path_provider_windows/example/README.md index 32f66a86d11d..63723991a2e9 100644 --- a/packages/path_provider/path_provider_windows/example/README.md +++ b/packages/path_provider/path_provider_windows/example/README.md @@ -1,8 +1,3 @@ # path_provider_windows_example Demonstrates how to use the path_provider_windows plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). 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 26a796fca90c..d48219648b30 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.8.0" dependencies: flutter: @@ -22,7 +22,6 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ 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/example/windows/runner/main.cpp b/packages/path_provider/path_provider_windows/example/windows/runner/main.cpp index 126302b0be18..c7dbde1c7123 100644 --- a/packages/path_provider/path_provider_windows/example/windows/runner/main.cpp +++ b/packages/path_provider/path_provider_windows/example/windows/runner/main.cpp @@ -11,7 +11,7 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _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()) { diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/utils.cpp b/packages/path_provider/path_provider_windows/example/windows/runner/utils.cpp index 537728149601..e875ce8b05a9 100644 --- a/packages/path_provider/path_provider_windows/example/windows/runner/utils.cpp +++ b/packages/path_provider/path_provider_windows/example/windows/runner/utils.cpp @@ -13,7 +13,7 @@ void CreateAndAttachConsole() { if (::AllocConsole()) { - FILE *unused; + FILE* unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } 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 2b87d51c1c49..6a9c138f5346 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 @@ -6,29 +6,51 @@ import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:path/path.dart' as path; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; 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) { @@ -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 0353574b6235..4e99be71da2f 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -1,12 +1,12 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin -repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows +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.3 +version: 2.1.0 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: @@ -14,13 +14,11 @@ flutter: platforms: windows: dartPluginClass: PathProviderWindows - pluginClass: none dependencies: - ffi: ^1.0.0 + ffi: ^2.0.0 flutter: sdk: flutter - meta: ^1.3.0 path: ^1.8.0 path_provider_platform_interface: ^2.0.0 win32: ^2.0.0 @@ -28,4 +26,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart index e977e07d99e6..571c31473a0b 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,12 +58,26 @@ 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', - }); + }, language: languageEn, 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', + 'ProductName': 'Amazing App', + }, language: languageEn, encoding: encodingUnicode); final String? path = await pathProvider.getApplicationSupportPath(); expect(path, isNotNull); if (path != null) { @@ -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 987049c55996..0e9b701444fd 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,27 @@ +## NEXT + +* Minor fixes for new analysis options. +* Adds additional tests for `PlatformInterface` and `MockPlatformInterfaceMixin`. + +## 2.1.2 + +* Updates README to demonstrate `verify` rather than `verifyToken`, and to note + that the test mixin applies to fakes as well as mocks. +* Adds an additional test for `verifyToken`. + +## 2.1.1 + +* Fixes `verify` to work with fake objects, not just mocks. + +## 2.1.0 + +* Introduce `verify`, which prevents use of `const Object()` as instance token. +* Add a comment indicating that `verifyToken` will be deprecated in a future release. + +## 2.0.2 + +* Update package description. + ## 2.0.1 * Fix `federated flutter plugins` link in the README.md. diff --git a/packages/plugin_platform_interface/README.md b/packages/plugin_platform_interface/README.md index 2fe44328c7dc..1b1f80425f76 100644 --- a/packages/plugin_platform_interface/README.md +++ b/packages/plugin_platform_interface/README.md @@ -25,7 +25,7 @@ abstract class UrlLauncherPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [UrlLauncherPlatform] when they register themselves. static set instance(UrlLauncherPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } @@ -35,14 +35,15 @@ abstract class UrlLauncherPlatform extends PlatformInterface { This guarantees that UrlLauncherPlatform.instance cannot be set to an object that `implements` UrlLauncherPlatform (it can only be set to an object that `extends` UrlLauncherPlatform). -## Mocking platform interfaces with Mockito +## Mocking or faking platform interfaces -Mockito mocks of platform interfaces will fail the verification done by `verifyToken`. -This package provides a `MockPlatformInterfaceMixin` which can be used in test code only to disable -the `extends` enforcement. +Test implementations of platform interfaces, such as those using `mockito`'s +`Mock` or `test`'s `Fake`, will fail the verification done by `verify`. +This package provides a `MockPlatformInterfaceMixin` which can be used in test +code only to disable the `extends` enforcement. -A Mockito mock of a platform interface can be created with: +For example, a Mockito mock of a platform interface can be created with: ```dart class UrlLauncherPlatformMock extends Mock diff --git a/packages/plugin_platform_interface/analysis_options.yaml b/packages/plugin_platform_interface/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/plugin_platform_interface/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart index d9bd88168422..a03c9ce2d367 100644 --- a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -12,7 +12,7 @@ import 'package:meta/meta.dart'; /// implemented using `extends` instead of `implements`. /// /// Platform interface classes are expected to have a private static token object which will be -/// be passed to [verifyToken] along with a platform interface object for verification. +/// be passed to [verify] along with a platform interface object for verification. /// /// Sample usage: /// @@ -22,14 +22,14 @@ import 'package:meta/meta.dart'; /// /// static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); /// -/// static const Object _token = Object(); +/// static final Object _token = Object(); /// /// static UrlLauncherPlatform get instance => _instance; /// /// /// Platform-specific plugins should set this with their own platform-specific /// /// class that extends [UrlLauncherPlatform] when they register themselves. /// static set instance(UrlLauncherPlatform instance) { -/// PlatformInterface.verifyToken(instance, _token); +/// PlatformInterface.verify(instance, _token); /// _instance = instance; /// } /// @@ -40,13 +40,16 @@ import 'package:meta/meta.dart'; /// to include the [MockPlatformInterfaceMixin] for the verification to be temporarily disabled. See /// [MockPlatformInterfaceMixin] for a sample of using Mockito to mock a platform interface. abstract class PlatformInterface { - /// Pass a private, class-specific `const Object()` as the `token`. + /// Constructs a PlatformInterface, for use only in constructors of abstract + /// derived classes. + /// + /// @param token The same, non-`const` `Object` that will be passed to `verify`. PlatformInterface({required Object token}) : _instanceToken = token; final Object? _instanceToken; - /// Ensures that the platform instance has a token that matches the - /// provided token and throws [AssertionError] if not. + /// Ensures that the platform instance was constructed with a non-`const` token + /// that matches the provided token and throws [AssertionError] if not. /// /// This is used to ensure that implementers are using `extends` rather than /// `implements`. @@ -56,7 +59,23 @@ abstract class PlatformInterface { /// /// This is implemented as a static method so that it cannot be overridden /// with `noSuchMethod`. + static void verify(PlatformInterface instance, Object token) { + _verify(instance, token, preventConstObject: true); + } + + /// Performs the same checks as `verify` but without throwing an + /// [AssertionError] if `const Object()` is used as the instance token. + /// + /// This method will be deprecated in a future release. static void verifyToken(PlatformInterface instance, Object token) { + _verify(instance, token, preventConstObject: false); + } + + static void _verify( + PlatformInterface instance, + Object token, { + required bool preventConstObject, + }) { if (instance is MockPlatformInterfaceMixin) { bool assertionsEnabled = false; assert(() { @@ -69,6 +88,10 @@ abstract class PlatformInterface { } return; } + if (preventConstObject && + identical(instance._instanceToken, const Object())) { + throw AssertionError('`const Object()` cannot be used as the token.'); + } if (!identical(token, instance._instanceToken)) { throw AssertionError( 'Platform interfaces must not be implemented with `implements`'); @@ -76,14 +99,15 @@ abstract class PlatformInterface { } } -/// A [PlatformInterface] mixin that can be combined with mockito's `Mock`. +/// A [PlatformInterface] mixin that can be combined with fake or mock objects, +/// such as test's `Fake` or mockito's `Mock`. /// -/// It passes the [PlatformInterface.verifyToken] check even though it isn't +/// It passes the [PlatformInterface.verify] check even though it isn't /// using `extends`. /// /// This class is intended for use in tests only. /// -/// Sample usage (assuming UrlLauncherPlatform extends [PlatformInterface]: +/// Sample usage (assuming `UrlLauncherPlatform` extends [PlatformInterface]): /// /// ```dart /// class UrlLauncherPlatformMock extends Mock diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index 2980a62ee998..1b601db1a388 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -1,6 +1,7 @@ name: plugin_platform_interface -description: Reusable base class for Flutter plugin platform interfaces. -repository: https://github.com/flutter/plugins/tree/master/packages/plugin_platform_interface +description: Reusable base class for platform interfaces of Flutter federated + plugins, to help enforce best practices. +repository: https://github.com/flutter/plugins/tree/main/packages/plugin_platform_interface issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+plugin_platform_interface%22 # DO NOT MAKE A BREAKING CHANGE TO THIS PACKAGE @@ -8,13 +9,13 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ # # This package is used as a second level dependency for many plugins, a major version bump here # is guaranteed to lead the ecosystem to a version lock (the first plugin that upgrades to version -# 2 of this package cannot be used with any other plugin that have not yet migrated). +# 3 of this package cannot be used with any other plugin that have not yet migrated). # # Please consider carefully before bumping the major version of this package, ideally it should only -# be done when absolutely necessary and after the ecosystem has already migrated to 1.X.Y version -# that is forward compatible with 2.0.0 (ideally the ecosystem have migrated to depend on: -# `plugin_platform_interface: >=1.X.Y <3.0.0`). -version: 2.0.1 +# 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 environment: sdk: ">=2.12.0 <3.0.0" @@ -24,5 +25,5 @@ dependencies: dev_dependencies: mockito: ^5.0.0 - test: ^1.16.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 0079765f0b09..329cecb16091 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -3,17 +3,17 @@ // found in the LICENSE file. import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/test.dart'; class SamplePluginPlatform extends PlatformInterface { SamplePluginPlatform() : super(token: _token); static final Object _token = Object(); + // ignore: avoid_setters_without_getters static set instance(SamplePluginPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); // A real implementation would set a static instance field here. } } @@ -25,22 +25,124 @@ class ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin extends Mock with MockPlatformInterfaceMixin implements SamplePluginPlatform {} +class ImplementsSamplePluginPlatformUsingFakePlatformInterfaceMixin extends Fake + with MockPlatformInterfaceMixin + implements SamplePluginPlatform {} + class ExtendsSamplePluginPlatform extends SamplePluginPlatform {} +class ConstTokenPluginPlatform extends PlatformInterface { + ConstTokenPluginPlatform() : super(token: _token); + + static const Object _token = Object(); // invalid + + // ignore: avoid_setters_without_getters + static set instance(ConstTokenPluginPlatform instance) { + PlatformInterface.verify(instance, _token); + } +} + +class ExtendsConstTokenPluginPlatform extends ConstTokenPluginPlatform {} + +class VerifyTokenPluginPlatform extends PlatformInterface { + VerifyTokenPluginPlatform() : super(token: _token); + + 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. + } +} + +class ImplementsVerifyTokenPluginPlatform extends Mock + implements VerifyTokenPluginPlatform {} + +class ImplementsVerifyTokenPluginPlatformUsingMockPlatformInterfaceMixin + extends Mock + with MockPlatformInterfaceMixin + implements VerifyTokenPluginPlatform {} + +class ExtendsVerifyTokenPluginPlatform extends VerifyTokenPluginPlatform {} + +class ConstVerifyTokenPluginPlatform extends PlatformInterface { + ConstVerifyTokenPluginPlatform() : super(token: _token); + + static const Object _token = Object(); // invalid + + // ignore: avoid_setters_without_getters + static set instance(ConstVerifyTokenPluginPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + } +} + +class ImplementsConstVerifyTokenPluginPlatform extends PlatformInterface + implements ConstVerifyTokenPluginPlatform { + ImplementsConstVerifyTokenPluginPlatform() : super(token: const Object()); +} + +// Ensures that `PlatformInterface` has no instance methods. Adding an +// instance method is discouraged and may be a breaking change if it +// conflicts with instance methods in subclasses. +class StaticMethodsOnlyPlatformInterfaceTest implements PlatformInterface {} + +class StaticMethodsOnlyMockPlatformInterfaceMixinTest + implements MockPlatformInterfaceMixin {} + void main() { - test('Cannot be implemented with `implements`', () { - expect(() { - SamplePluginPlatform.instance = ImplementsSamplePluginPlatform(); - }, throwsA(isA())); - }); + group('`verify`', () { + test('prevents implementation with `implements`', () { + expect(() { + SamplePluginPlatform.instance = ImplementsSamplePluginPlatform(); + }, throwsA(isA())); + }); + + test('allows mocking with `implements`', () { + final SamplePluginPlatform mock = + ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); + SamplePluginPlatform.instance = mock; + }); - test('Can be mocked with `implements`', () { - final SamplePluginPlatform mock = - ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); - SamplePluginPlatform.instance = mock; + test('allows faking with `implements`', () { + final SamplePluginPlatform fake = + ImplementsSamplePluginPlatformUsingFakePlatformInterfaceMixin(); + SamplePluginPlatform.instance = fake; + }); + + test('allows extending', () { + SamplePluginPlatform.instance = ExtendsSamplePluginPlatform(); + }); + + test('prevents `const Object()` token', () { + expect(() { + ConstTokenPluginPlatform.instance = ExtendsConstTokenPluginPlatform(); + }, throwsA(isA())); + }); }); - test('Can be extended', () { - SamplePluginPlatform.instance = ExtendsSamplePluginPlatform(); + // Tests of the earlier, to-be-deprecated `verifyToken` method + group('`verifyToken`', () { + test('prevents implementation with `implements`', () { + expect(() { + VerifyTokenPluginPlatform.instance = + ImplementsVerifyTokenPluginPlatform(); + }, throwsA(isA())); + }); + + test('allows mocking with `implements`', () { + final VerifyTokenPluginPlatform mock = + ImplementsVerifyTokenPluginPlatformUsingMockPlatformInterfaceMixin(); + VerifyTokenPluginPlatform.instance = mock; + }); + + test('allows extending', () { + VerifyTokenPluginPlatform.instance = ExtendsVerifyTokenPluginPlatform(); + }); + + test('does not prevent `const Object()` token', () { + ConstVerifyTokenPluginPlatform.instance = + ImplementsConstVerifyTokenPluginPlatform(); + }); }); } diff --git a/packages/quick_actions/analysis_options.yaml b/packages/quick_actions/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/quick_actions/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index d2d628cad428..d7703a85a548 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,30 @@ +## NEXT + +* 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. +* Updates code for analyzer changes. +* Removes dependency on `meta`. + +## 0.6.0+8 + +* Updates example app Android compileSdkVersion to 31. +* Moves method call to background thread to fix CI failure. + ## 0.6.0+7 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/quick_actions/quick_actions/README.md b/packages/quick_actions/quick_actions/README.md index 46e87fa0b241..10bae164d534 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,7 @@ 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 - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). +### Android -For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). +\* The plugin will compile and run on SDK 16+, but will be a no-op below SDK 25 +(Android 7.1). diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle deleted file mode 100644 index ec3f84eab4cf..000000000000 --- a/packages/quick_actions/quick_actions/android/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -group 'io.flutter.plugins.quickactions' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:3.2.4' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/quick_actions/quick_actions/android/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions/android/src/main/AndroidManifest.xml deleted file mode 100644 index 5b02f6d8aef2..000000000000 --- a/packages/quick_actions/quick_actions/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java deleted file mode 100644 index 2d89352f3e09..000000000000 --- a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ /dev/null @@ -1,130 +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.quickactions; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.content.res.Resources; -import android.graphics.drawable.Icon; -import android.os.Build; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - protected static final String EXTRA_ACTION = "some unique action key"; - private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - - private final Context context; - private Activity activity; - - MethodCallHandlerImpl(Context context, Activity activity) { - this.context = context; - this.activity = activity; - } - - void setActivity(Activity activity) { - this.activity = activity; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { - // We already know that this functionality does not work for anything - // lower than API 25 so we chose not to return error. Instead we do nothing. - result.success(null); - return; - } - ShortcutManager shortcutManager = - (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); - switch (call.method) { - case "setShortcutItems": - List> serializedShortcuts = call.arguments(); - List shortcuts = deserializeShortcuts(serializedShortcuts); - shortcutManager.setDynamicShortcuts(shortcuts); - break; - case "clearShortcutItems": - shortcutManager.removeAllDynamicShortcuts(); - break; - case "getLaunchAction": - if (activity == null) { - result.error( - "quick_action_getlaunchaction_no_activity", - "There is no activity available when launching action", - null); - return; - } - final Intent intent = activity.getIntent(); - final String launchAction = intent.getStringExtra(EXTRA_ACTION); - if (launchAction != null && !launchAction.isEmpty()) { - shortcutManager.reportShortcutUsed(launchAction); - intent.removeExtra(EXTRA_ACTION); - } - result.success(launchAction); - return; - default: - result.notImplemented(); - return; - } - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.N_MR1) - private List deserializeShortcuts(List> shortcuts) { - final List shortcutInfos = new ArrayList<>(); - - for (Map shortcut : shortcuts) { - final String icon = shortcut.get("icon"); - final String type = shortcut.get("type"); - final String title = shortcut.get("localizedTitle"); - final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); - - final int resourceId = loadResourceId(context, icon); - final Intent intent = getIntentToOpenMainActivity(type); - - if (resourceId > 0) { - shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); - } - - final ShortcutInfo shortcutInfo = - shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); - shortcutInfos.add(shortcutInfo); - } - return shortcutInfos; - } - - private int loadResourceId(Context context, String icon) { - if (icon == null) { - return 0; - } - final String packageName = context.getPackageName(); - final Resources res = context.getResources(); - final int resourceId = res.getIdentifier(icon, "drawable", packageName); - - if (resourceId == 0) { - return res.getIdentifier(icon, "mipmap", packageName); - } else { - return resourceId; - } - } - - private Intent getIntentToOpenMainActivity(String type) { - final String packageName = context.getPackageName(); - - return context - .getPackageManager() - .getLaunchIntentForPackage(packageName) - .setAction(Intent.ACTION_RUN) - .putExtra(EXTRA_ACTION, type) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - } -} diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java deleted file mode 100644 index 208a119efafe..000000000000 --- a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ /dev/null @@ -1,165 +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.quickactions; - -import static io.flutter.plugins.quickactions.MethodCallHandlerImpl.EXTRA_ACTION; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.Intent; -import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMethodCodec; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -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 { - public MethodCall lastMethodCall; - - @Override - public void send(@NonNull String channel, @Nullable ByteBuffer message) { - send(channel, message, null); - } - - @Override - public void send( - @NonNull String channel, - @Nullable ByteBuffer message, - @Nullable final BinaryReply callback) { - if (channel.equals("plugins.flutter.io/quick_actions")) { - lastMethodCall = - StandardMethodCodec.INSTANCE.decodeMethodCall((ByteBuffer) message.position(0)); - } - } - - @Override - public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { - // Do nothing. - } - } - - static final int SUPPORTED_BUILD = 25; - static final int UNSUPPORTED_BUILD = 24; - static final String SHORTCUT_TYPE = "action_one"; - - @Test - public void canAttachToEngine() { - final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); - final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); - when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); - - final QuickActionsPlugin plugin = new QuickActionsPlugin(); - plugin.onAttachedToEngine(mockPluginBinding); - } - - @Test - public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() - throws NoSuchFieldException, IllegalAccessException { - // Arrange - final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); - final QuickActionsPlugin plugin = new QuickActionsPlugin(); - setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); - setBuildVersion(SUPPORTED_BUILD); - FieldSetter.setField( - plugin, - QuickActionsPlugin.class.getDeclaredField("handler"), - 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); - - // Act - plugin.onAttachedToActivity(mockActivityPluginBinding); - - // Assert - assertNotNull(testBinaryMessenger.lastMethodCall); - assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); - assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); - } - - @Test - public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod() - throws NoSuchFieldException, IllegalAccessException { - // Arrange - final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); - final QuickActionsPlugin plugin = new QuickActionsPlugin(); - setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); - setBuildVersion(UNSUPPORTED_BUILD); - final Intent mockIntent = createMockIntentWithQuickActionExtra(); - - // Act - final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); - - // Assert - assertNull(testBinaryMessenger.lastMethodCall); - assertFalse(onNewIntentReturn); - } - - @Test - public void onNewIntent_buildVersionSupported_invokesLaunchMethod() - throws NoSuchFieldException, IllegalAccessException { - // Arrange - final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); - final QuickActionsPlugin plugin = new QuickActionsPlugin(); - setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); - setBuildVersion(SUPPORTED_BUILD); - final Intent mockIntent = createMockIntentWithQuickActionExtra(); - - // Act - final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); - - // Assert - assertNotNull(testBinaryMessenger.lastMethodCall); - assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); - assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); - assertFalse(onNewIntentReturn); - } - - private void setUpMessengerAndFlutterPluginBinding( - TestBinaryMessenger testBinaryMessenger, QuickActionsPlugin plugin) { - final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); - when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); - plugin.onAttachedToEngine(mockPluginBinding); - } - - private Intent createMockIntentWithQuickActionExtra() { - final Intent mockIntent = mock(Intent.class); - when(mockIntent.hasExtra(EXTRA_ACTION)).thenReturn(true); - when(mockIntent.getStringExtra(EXTRA_ACTION)).thenReturn(QuickActionsTest.SHORTCUT_TYPE); - return mockIntent; - } - - private void setBuildVersion(int buildVersion) - throws NoSuchFieldException, IllegalAccessException { - Field buildSdkField = Build.VERSION.class.getField("SDK_INT"); - buildSdkField.setAccessible(true); - final Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(buildSdkField, buildSdkField.getModifiers() & ~Modifier.FINAL); - buildSdkField.set(null, buildVersion); - } - - @After - public void tearDown() throws NoSuchFieldException, IllegalAccessException { - setBuildVersion(0); - } -} 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 485ae5511063..75fe3543e987 100644 --- a/packages/quick_actions/quick_actions/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions/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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -33,7 +33,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.quickactionsexample" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -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/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java deleted file mode 100644 index 9d2fed13fc27..000000000000 --- a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.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.quickactionsexample; - -import static org.junit.Assert.assertTrue; - -import androidx.test.core.app.ActivityScenario; -import io.flutter.plugins.quickactions.QuickActionsPlugin; -import org.junit.Test; - -public class QuickActionsTest { - @Test - public void imagePickerPluginIsAdded() { - final ActivityScenario scenario = - ActivityScenario.launch(QuickActionsTestActivity.class); - scenario.onActivity( - activity -> { - assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class)); - }); - } -} 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 cfe3eb0db656..bfefef3b298b 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 @@ -11,7 +11,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Can set shortcuts', (WidgetTester tester) async { - final QuickActions quickActions = QuickActions(); + const QuickActions quickActions = QuickActions(); await quickActions.initialize(null); const ShortcutItem shortCutItem = ShortcutItem( diff --git a/packages/quick_actions/quick_actions/example/ios/Runner/main.m b/packages/quick_actions/quick_actions/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner/main.m +++ b/packages/quick_actions/quick_actions/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m b/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m index 64e0f7e1d8b2..ddcdc6a8defc 100644 --- a/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m +++ b/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m @@ -11,7 +11,7 @@ @interface QuickActionsTests : XCTestCase @implementation QuickActionsTests - (void)testPlugin { - FLTQuickActionsPlugin* plugin = [[FLTQuickActionsPlugin alloc] init]; + FLTQuickActionsPlugin *plugin = [[FLTQuickActionsPlugin alloc] init]; XCTAssertNotNil(plugin); } diff --git a/packages/quick_actions/quick_actions/example/lib/main.dart b/packages/quick_actions/quick_actions/example/lib/main.dart index 8e47d16683dd..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( @@ -19,16 +21,16 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(), + home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key}) : super(key: key); + const MyHomePage({Key? key}) : super(key: key); @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -38,7 +40,7 @@ class _MyHomePageState extends State { void initState() { super.initState(); - final QuickActions quickActions = QuickActions(); + const QuickActions quickActions = QuickActions(); quickActions.initialize((String shortcutType) { setState(() { if (shortcutType != null) { @@ -61,7 +63,7 @@ class _MyHomePageState extends State { type: 'action_two', localizedTitle: 'Action two', icon: 'ic_launcher'), - ]).then((value) { + ]).then((void _) { setState(() { if (shortcut == 'no action set') { shortcut = 'actions ready'; @@ -74,7 +76,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('$shortcut'), + title: Text(shortcut), ), body: const Center( child: Text('On home screen, long press the app icon to ' diff --git a/packages/quick_actions/quick_actions/example/pubspec.yaml b/packages/quick_actions/quick_actions/example/pubspec.yaml index c4ee86039761..64e61b71e720 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.8.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions/ios/Assets/.gitkeep b/packages/quick_actions/quick_actions/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/quick_actions/quick_actions/ios/quick_actions.podspec b/packages/quick_actions/quick_actions/ios/quick_actions.podspec deleted file mode 100644 index 9452fd8c983d..000000000000 --- a/packages/quick_actions/quick_actions/ios/quick_actions.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'quick_actions' - s.version = '0.0.1' - s.summary = 'Flutter Quick Actions' - s.description = <<-DESC -This Flutter plugin allows you to manage and interact with the application's home screen quick actions. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/quick_actions' } - s.documentation_url = 'https://pub.dev/packages/quick_actions' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 9531b7027cdf..37e8dbe5f3e3 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -1,27 +1,27 @@ name: quick_actions 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/master/packages/quick_actions/quick_actions +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+7 +version: 0.6.0+11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.quickactions - pluginClass: QuickActionsPlugin + default_package: quick_actions_android ios: - pluginClass: FLTQuickActionsPlugin + default_package: quick_actions_ios dependencies: flutter: sdk: flutter - meta: ^1.3.0 + quick_actions_android: ^0.6.0+9 + quick_actions_ios: ^0.6.0+9 quick_actions_platform_interface: ^1.0.0 dev_dependencies: @@ -29,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 27d3c81a809a..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(); - QuickActionHandler handler = (type) {}; + void handler(String type) {} await quickActions.initialize(handler); verify(QuickActionsPlatform.instance.initialize(handler)).called(1); @@ -31,17 +29,18 @@ void main() { test('setShortcutItems() PlatformInterface', () { const QuickActions quickActions = QuickActions(); - QuickActionHandler handler = (type) {}; + void handler(String type) {} quickActions.initialize(handler); - quickActions.setShortcutItems([]); + quickActions.setShortcutItems([]); verify(QuickActionsPlatform.instance.initialize(handler)).called(1); - verify(QuickActionsPlatform.instance.setShortcutItems([])).called(1); + verify(QuickActionsPlatform.instance.setShortcutItems([])) + .called(1); }); test('clearShortcutItems() PlatformInterface', () { const QuickActions quickActions = QuickActions(); - QuickActionHandler handler = (type) {}; + void handler(String type) {} quickActions.initialize(handler); quickActions.clearShortcutItems(); @@ -57,15 +56,15 @@ class MockQuickActionsPlatform extends Mock implements QuickActionsPlatform { @override Future clearShortcutItems() async => - super.noSuchMethod(Invocation.method(#clearShortcutItems, [])); + super.noSuchMethod(Invocation.method(#clearShortcutItems, [])); @override Future initialize(QuickActionHandler? handler) async => - super.noSuchMethod(Invocation.method(#initialize, [handler])); + super.noSuchMethod(Invocation.method(#initialize, [handler])); @override - Future setShortcutItems(List? items) async => - super.noSuchMethod(Invocation.method(#setShortcutItems, [items])); + Future setShortcutItems(List? items) async => super + .noSuchMethod(Invocation.method(#setShortcutItems, [items])); } class MockQuickActions extends QuickActions {} diff --git a/packages/quick_actions/quick_actions_android/AUTHORS b/packages/quick_actions/quick_actions_android/AUTHORS new file mode 100644 index 000000000000..5f17b78d134f --- /dev/null +++ b/packages/quick_actions/quick_actions_android/AUTHORS @@ -0,0 +1,68 @@ +# 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> +Daniel Roek +Maurits van Beusekom diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md new file mode 100644 index 000000000000..5b5f70946e91 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -0,0 +1,16 @@ +## 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. diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/LICENSE b/packages/quick_actions/quick_actions_android/LICENSE similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter_platform_interface/LICENSE rename to packages/quick_actions/quick_actions_android/LICENSE diff --git a/packages/quick_actions/quick_actions_android/README.md b/packages/quick_actions/quick_actions_android/README.md new file mode 100644 index 000000000000..8b7fc8895212 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/README.md @@ -0,0 +1,17 @@ +# quick\_actions\_android + +The Android implementation of [`quick_actions`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `quick_actions` +normally. This package will be automatically included in your app when you do. + +## Contributing + +If you would like to contribute to the plugin, check out our [contribution guide][3]. + +[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/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 new file mode 100644 index 000000000000..252a35246aa9 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'io.flutter.plugins.quickactions' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.2.4' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/quick_actions/quick_actions/android/settings.gradle b/packages/quick_actions/quick_actions_android/android/settings.gradle similarity index 100% rename from packages/quick_actions/quick_actions/android/settings.gradle rename to packages/quick_actions/quick_actions_android/android/settings.gradle 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 new file mode 100644 index 000000000000..5ec81f08ec6a --- /dev/null +++ b/packages/quick_actions/quick_actions_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + 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 new file mode 100644 index 000000000000..96b141fb9c31 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -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. + +package io.flutter.plugins.quickactions; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + protected static final String EXTRA_ACTION = "some unique action key"; + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions_android"; + + private final Context context; + private Activity activity; + + MethodCallHandlerImpl(Context context, Activity activity) { + this.context = context; + this.activity = activity; + } + + void setActivity(Activity activity) { + this.activity = activity; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // We already know that this functionality does not work for anything + // lower than API 25 so we chose not to return error. Instead we do nothing. + result.success(null); + return; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + switch (call.method) { + case "setShortcutItems": + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + List> serializedShortcuts = call.arguments(); + List shortcuts = deserializeShortcuts(serializedShortcuts); + + Executor uiThreadExecutor = new UiThreadExecutor(); + ThreadPoolExecutor executor = + new ThreadPoolExecutor( + 0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue()); + + executor.execute( + () -> { + boolean dynamicShortcutsSet = false; + try { + shortcutManager.setDynamicShortcuts(shortcuts); + dynamicShortcutsSet = true; + } catch (Exception e) { + // Leave dynamicShortcutsSet as false + } + + final boolean didSucceed = dynamicShortcutsSet; + + // TODO(camsim99): Move re-dispatch below to background thread when Flutter 2.8+ is + // stable. + uiThreadExecutor.execute( + () -> { + if (didSucceed) { + result.success(null); + } else { + result.error( + "quick_action_setshortcutitems_failure", + "Exception thrown when setting dynamic shortcuts", + null); + } + }); + }); + } + return; + case "clearShortcutItems": + shortcutManager.removeAllDynamicShortcuts(); + break; + case "getLaunchAction": + if (activity == null) { + result.error( + "quick_action_getlaunchaction_no_activity", + "There is no activity available when launching action", + null); + return; + } + final Intent intent = activity.getIntent(); + final String launchAction = intent.getStringExtra(EXTRA_ACTION); + if (launchAction != null && !launchAction.isEmpty()) { + shortcutManager.reportShortcutUsed(launchAction); + intent.removeExtra(EXTRA_ACTION); + } + result.success(launchAction); + return; + default: + result.notImplemented(); + return; + } + result.success(null); + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + private List deserializeShortcuts(List> shortcuts) { + final List shortcutInfos = new ArrayList<>(); + + for (Map shortcut : shortcuts) { + final String icon = shortcut.get("icon"); + final String type = shortcut.get("type"); + final String title = shortcut.get("localizedTitle"); + final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); + + final int resourceId = loadResourceId(context, icon); + final Intent intent = getIntentToOpenMainActivity(type); + + if (resourceId > 0) { + shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); + } + + final ShortcutInfo shortcutInfo = + shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); + shortcutInfos.add(shortcutInfo); + } + return shortcutInfos; + } + + private int loadResourceId(Context context, String icon) { + if (icon == null) { + return 0; + } + final String packageName = context.getPackageName(); + final Resources res = context.getResources(); + final int resourceId = res.getIdentifier(icon, "drawable", packageName); + + if (resourceId == 0) { + return res.getIdentifier(icon, "mipmap", packageName); + } else { + return resourceId; + } + } + + private Intent getIntentToOpenMainActivity(String type) { + final String packageName = context.getPackageName(); + + return context + .getPackageManager() + .getLaunchIntentForPackage(packageName) + .setAction(Intent.ACTION_RUN) + .putExtra(EXTRA_ACTION, type) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + 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/quick_actions/quick_actions/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 similarity index 83% rename from packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java rename to packages/quick_actions/quick_actions_android/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index b2f80ad0a271..b41087816889 100644 --- a/packages/quick_actions/quick_actions/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; @@ -17,10 +18,11 @@ /** QuickActionsPlugin */ public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewIntentListener { - private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions_android"; 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 new file mode 100644 index 000000000000..dc4b36e168db --- /dev/null +++ b/packages/quick_actions/quick_actions_android/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -0,0 +1,181 @@ +// Copyright 2013 The Flutter 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.quickactions; + +import static io.flutter.plugins.quickactions.MethodCallHandlerImpl.EXTRA_ACTION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +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; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMethodCodec; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +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 { + public MethodCall lastMethodCall; + + @Override + public void send(@NonNull String channel, @Nullable ByteBuffer message) { + send(channel, message, null); + } + + @Override + public void send( + @NonNull String channel, + @Nullable ByteBuffer message, + @Nullable final BinaryReply callback) { + if (channel.equals("plugins.flutter.io/quick_actions_android")) { + lastMethodCall = + StandardMethodCodec.INSTANCE.decodeMethodCall((ByteBuffer) message.position(0)); + } + } + + @Override + public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { + // Do nothing. + } + } + + static final int SUPPORTED_BUILD = 25; + static final int UNSUPPORTED_BUILD = 24; + static final String SHORTCUT_TYPE = "action_one"; + + @Test + public void canAttachToEngine() { + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.onAttachedToEngine(mockPluginBinding); + } + + @Test + public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + FieldSetter.setField( + plugin, + QuickActionsPlugin.class.getDeclaredField("handler"), + 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); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + } + + @Test + public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(UNSUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNull(testBinaryMessenger.lastMethodCall); + assertFalse(onNewIntentReturn); + } + + @Test + public void onNewIntent_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + 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); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + assertFalse(onNewIntentReturn); + } + + private void setUpMessengerAndFlutterPluginBinding( + TestBinaryMessenger testBinaryMessenger, QuickActionsPlugin plugin) { + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + plugin.onAttachedToEngine(mockPluginBinding); + } + + private Intent createMockIntentWithQuickActionExtra() { + final Intent mockIntent = mock(Intent.class); + when(mockIntent.hasExtra(EXTRA_ACTION)).thenReturn(true); + when(mockIntent.getStringExtra(EXTRA_ACTION)).thenReturn(QuickActionsTest.SHORTCUT_TYPE); + return mockIntent; + } + + private void setBuildVersion(int buildVersion) + throws NoSuchFieldException, IllegalAccessException { + Field buildSdkField = Build.VERSION.class.getField("SDK_INT"); + buildSdkField.setAccessible(true); + final Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(buildSdkField, buildSdkField.getModifiers() & ~Modifier.FINAL); + buildSdkField.set(null, buildVersion); + } + + @After + public void tearDown() throws NoSuchFieldException, IllegalAccessException { + setBuildVersion(0); + } +} diff --git a/packages/quick_actions/quick_actions_android/example/README.md b/packages/quick_actions/quick_actions_android/example/README.md new file mode 100644 index 000000000000..c8a629019fc9 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/README.md @@ -0,0 +1,3 @@ +# quick_actions_example + +Demonstrates how to use the quick_actions plugin. 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 new file mode 100644 index 000000000000..75920e00fcab --- /dev/null +++ b/packages/quick_actions/quick_actions_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" + +def androidXTestVersion = '1.2.0' + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.quickactionsexample" + minSdkVersion 21 + 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.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.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.3.1' + androidTestImplementation 'org.mockito:mockito-android:4.3.1' +} diff --git a/packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/quick_actions/quick_actions_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java new file mode 100644 index 000000000000..e96548da291a --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/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.quickactionsexample; + +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/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 new file mode 100644 index 000000000000..8b50fd7a90eb --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java @@ -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. + +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.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 quickActionPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(QuickActionsTestActivity.class); + scenario.onActivity( + activity -> { + 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()); + } + } + + @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/app/src/debug/AndroidManifest.xml b/packages/quick_actions/quick_actions_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..bee689df1735 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..4f384b7c6b13 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java b/packages/quick_actions/quick_actions_android/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java new file mode 100644 index 000000000000..4ff3a27cd5c0 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.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.quickactionsexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class QuickActionsTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/drawable/ic_launcher_background.xml b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..9ed346888001 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/package_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/package_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/package_info/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/package_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/package_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/package_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/quick_actions/quick_actions_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/values/styles.xml b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..6c1d1ec695c9 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/battery/battery/example/android/build.gradle b/packages/quick_actions/quick_actions_android/example/android/build.gradle similarity index 100% rename from packages/battery/battery/example/android/build.gradle rename to packages/quick_actions/quick_actions_android/example/android/build.gradle diff --git a/packages/device_info/device_info/example/android/gradle.properties b/packages/quick_actions/quick_actions_android/example/android/gradle.properties similarity index 100% rename from packages/device_info/device_info/example/android/gradle.properties rename to packages/quick_actions/quick_actions_android/example/android/gradle.properties diff --git a/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions_android/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/quick_actions/quick_actions_android/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/package_info/example/android/settings.gradle b/packages/quick_actions/quick_actions_android/example/android/settings.gradle similarity index 100% rename from packages/package_info/example/android/settings.gradle rename to packages/quick_actions/quick_actions_android/example/android/settings.gradle 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 new file mode 100644 index 000000000000..e0abe90f75aa --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/integration_test/quick_actions_test.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/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:quick_actions_example/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can run MyApp', (WidgetTester tester) async { + app.main(); + + 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 new file mode 100644 index 000000000000..8f66e69ffb4e --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/lib/main.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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:quick_actions_android/quick_actions_android.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Quick Actions Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key}) : super(key: key); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + String shortcut = 'no action set'; + + @override + void initState() { + super.initState(); + + final QuickActionsAndroid quickActions = QuickActionsAndroid(); + quickActions.initialize((String shortcutType) { + setState(() { + if (shortcutType != null) { + shortcut = '$shortcutType has launched'; + } + }); + }); + + quickActions.setShortcutItems([ + const ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ), + const ShortcutItem( + type: 'action_two', + localizedTitle: 'Action two', + icon: 'ic_launcher'), + ]).then((void _) { + setState(() { + if (shortcut == 'no action set') { + shortcut = 'actions ready'; + } + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(shortcut), + ), + body: const Center( + child: Text('On home screen, long press the app icon to ' + 'get Action one or Action two options. Tapping on that action should ' + 'set the toolbar title.'), + ), + ); + } +} diff --git a/packages/quick_actions/quick_actions_android/example/pubspec.yaml b/packages/quick_actions/quick_actions_android/example/pubspec.yaml new file mode 100644 index 000000000000..53170971d136 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: quick_actions_example +description: Demonstrates how to use the quick_actions plugin. +publish_to: none + +environment: + sdk: ">=2.15.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + quick_actions_android: + # When depending on this package from a real application you should use: + # quick_actions_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: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/quick_actions/quick_actions_android/example/test_driver/integration_test.dart b/packages/quick_actions/quick_actions_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/quick_actions/quick_actions_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/quick_actions/quick_actions_android/lib/quick_actions_android.dart b/packages/quick_actions/quick_actions_android/lib/quick_actions_android.dart new file mode 100644 index 000000000000..99a54e9866af --- /dev/null +++ b/packages/quick_actions/quick_actions_android/lib/quick_actions_android.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. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; + +export 'package:quick_actions_platform_interface/types/types.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/quick_actions_android'); + +/// An implementation of [QuickActionsPlatform] that for Android. +class QuickActionsAndroid extends QuickActionsPlatform { + /// Registers this class as the default instance of [QuickActionsPlatform]. + static void registerWith() { + QuickActionsPlatform.instance = QuickActionsAndroid(); + } + + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + @override + Future initialize(QuickActionHandler handler) async { + channel.setMethodCallHandler((MethodCall call) async { + assert(call.method == 'launch'); + handler(call.arguments as String); + }); + final String? action = + await channel.invokeMethod('getLaunchAction'); + if (action != null) { + handler(action); + } + } + + @override + Future setShortcutItems(List items) async { + final List> itemsList = + items.map(_serializeItem).toList(); + await channel.invokeMethod('setShortcutItems', itemsList); + } + + @override + Future clearShortcutItems() => + channel.invokeMethod('clearShortcutItems'); + + Map _serializeItem(ShortcutItem item) { + return { + 'type': item.type, + 'localizedTitle': item.localizedTitle, + 'icon': item.icon, + }; + } +} diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml new file mode 100644 index 000000000000..7cb274116536 --- /dev/null +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -0,0 +1,30 @@ +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.1 + +environment: + sdk: ">=2.15.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: quick_actions + platforms: + android: + package: io.flutter.plugins.quickactions + pluginClass: QuickActionsPlugin + dartPluginClass: QuickActionsAndroid + +dependencies: + flutter: + sdk: flutter + quick_actions_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + plugin_platform_interface: ^2.1.2 diff --git a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart new file mode 100644 index 000000000000..40cfe458615d --- /dev/null +++ b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter Authors. 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:quick_actions_android/quick_actions_android.dart'; +import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$QuickActionsAndroid', () { + late List log; + + setUp(() { + log = []; + }); + + QuickActionsAndroid buildQuickActionsPlugin() { + final QuickActionsAndroid quickActions = QuickActionsAndroid(); + quickActions.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + return quickActions; + } + + test('registerWith() registers correct instance', () { + QuickActionsAndroid.registerWith(); + expect(QuickActionsPlatform.instance, isA()); + }); + + group('#initialize', () { + test('passes getLaunchAction on launch method', () { + final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); + quickActions.initialize((String type) {}); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + ], + ); + }); + + test('initialize', () async { + final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); + final Completer quickActionsHandler = Completer(); + await quickActions + .initialize((_) => quickActionsHandler.complete(true)); + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + ], + ); + log.clear(); + + expect(quickActionsHandler.future, completion(isTrue)); + }); + }); + + group('#setShortCutItems', () { + test('passes shortcutItem through channel', () { + final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); + quickActions.initialize((String type) {}); + quickActions.setShortcutItems([ + const ShortcutItem( + type: 'test', localizedTitle: 'title', icon: 'icon.svg') + ]); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + isMethodCall('setShortcutItems', arguments: >[ + { + 'type': 'test', + 'localizedTitle': 'title', + 'icon': 'icon.svg', + } + ]), + ], + ); + }); + + test('setShortcutItems with demo data', () async { + const String type = 'type'; + const String localizedTitle = 'localizedTitle'; + const String icon = 'icon'; + final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); + await quickActions.setShortcutItems( + const [ + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon) + ], + ); + expect( + log, + [ + isMethodCall( + 'setShortcutItems', + arguments: >[ + { + 'type': type, + 'localizedTitle': localizedTitle, + 'icon': icon, + } + ], + ), + ], + ); + log.clear(); + }); + }); + + group('#clearShortCutItems', () { + test('send clearShortcutItems through channel', () { + final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); + quickActions.initialize((String type) {}); + quickActions.clearShortcutItems(); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + isMethodCall('clearShortcutItems', arguments: null), + ], + ); + }); + + test('clearShortcutItems', () { + final QuickActionsAndroid quickActions = buildQuickActionsPlugin(); + quickActions.clearShortcutItems(); + expect( + log, + [ + isMethodCall('clearShortcutItems', arguments: null), + ], + ); + log.clear(); + }); + }); + }); + + group('$ShortcutItem', () { + test('Shortcut item can be constructed', () { + const String type = 'type'; + const String localizedTitle = 'title'; + const String icon = 'foo'; + + const ShortcutItem item = + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon); + + expect(item.type, type); + expect(item.localizedTitle, localizedTitle); + expect(item.icon, icon); + }); + }); +} diff --git a/packages/quick_actions/quick_actions_ios/AUTHORS b/packages/quick_actions/quick_actions_ios/AUTHORS new file mode 100644 index 000000000000..5f17b78d134f --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/AUTHORS @@ -0,0 +1,68 @@ +# 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> +Daniel Roek +Maurits van Beusekom diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md new file mode 100644 index 000000000000..35f9c9fd51d9 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -0,0 +1,12 @@ +## 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. diff --git a/packages/quick_actions/quick_actions_ios/LICENSE b/packages/quick_actions/quick_actions_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/quick_actions/quick_actions_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/quick_actions/quick_actions_ios/README.md b/packages/quick_actions/quick_actions_ios/README.md new file mode 100644 index 000000000000..e33b9ec3ab14 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/README.md @@ -0,0 +1,16 @@ +# quick\_actions\_ios + +The iOS implementation of [`quick_actions`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `quick_actions` +normally. This package will be automatically included in your app when you do. + +## Contributing + +If you would like to contribute to the plugin, check out our [contribution guide][3]. + +[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/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 new file mode 100644 index 000000000000..c8a629019fc9 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/README.md @@ -0,0 +1,3 @@ +# quick_actions_example + +Demonstrates how to use the quick_actions plugin. diff --git a/packages/quick_actions/quick_actions_ios/example/integration_test/quick_actions_test.dart b/packages/quick_actions/quick_actions_ios/example/integration_test/quick_actions_test.dart new file mode 100644 index 000000000000..b89c09d639d3 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/integration_test/quick_actions_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:integration_test/integration_test.dart'; +import 'package:quick_actions_ios/quick_actions_ios.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can set shortcuts', (WidgetTester tester) async { + final QuickActionsIos quickActions = QuickActionsIos(); + await quickActions.initialize((String value) {}); + + const ShortcutItem shortCutItem = ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ); + expect( + quickActions.setShortcutItems([shortCutItem]), completes); + }); +} diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/quick_actions/quick_actions_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/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig rename to packages/quick_actions/quick_actions_ios/example/ios/Flutter/Debug.xcconfig diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig b/packages/quick_actions/quick_actions_ios/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig rename to packages/quick_actions/quick_actions_ios/example/ios/Flutter/Release.xcconfig diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Podfile b/packages/quick_actions/quick_actions_ios/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/Podfile @@ -0,0 +1,41 @@ +# 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 + 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/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..36dc0d81923b --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + 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 */; }; + 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 */; }; + 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 */ + 33E20B3726EFCDFC00A4A191 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + 686BE83225E58CCF00862533 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + 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 = ""; }; + 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 = ""; }; + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 686BE82F25E58CCF00862533 /* RunnerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerUITests.m; sourceTree = ""; }; + 686BE83125E58CCF00862533 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 96F949A6B78E2DC62B93C4F8 /* 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 = ""; }; + 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 = ""; }; + 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; }; + 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 */ + +/* Begin PBXFrameworksBuildPhase section */ + 33E20B2F26EFCDFC00A4A191 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 686BE82A25E58CCF00862533 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33E20B3326EFCDFC00A4A191 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */, + 33E20B3626EFCDFC00A4A191 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 686BE82E25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 686BE82F25E58CCF00862533 /* RunnerUITests.m */, + 686BE83125E58CCF00862533 /* Info.plist */, + ); + path = RunnerUITests; + 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 */, + 686BE82E25E58CCF00862533 /* RunnerUITests */, + 33E20B3326EFCDFC00A4A191 /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + D0FE95BE2380323DD75CB891 /* Pods */, + A44AD0D63DEF785A2A2DEE28 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */, + 33E20B3226EFCDFC00A4A191 /* 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 = ""; + }; + A44AD0D63DEF785A2A2DEE28 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */, + D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0FE95BE2380323DD75CB891 /* Pods */ = { + isa = PBXGroup; + children = ( + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */, + F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */, + 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */, + 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33E20B3126EFCDFC00A4A191 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 3B2E8279C112D7129C8D23F1 /* [CP] Check Pods Manifest.lock */, + 33E20B2E26EFCDFC00A4A191 /* Sources */, + 33E20B2F26EFCDFC00A4A191 /* Frameworks */, + 33E20B3026EFCDFC00A4A191 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33E20B3826EFCDFC00A4A191 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 686BE82C25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 686BE82925E58CCF00862533 /* Sources */, + 686BE82A25E58CCF00862533 /* Frameworks */, + 686BE82B25E58CCF00862533 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 686BE83325E58CCF00862533 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C6989ECD8FF0836301D734B4 /* [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 = { + 33E20B3126EFCDFC00A4A191 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 686BE82C25E58CCF00862533 = { + 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 */, + 686BE82C25E58CCF00862533 /* RunnerUITests */, + 33E20B3126EFCDFC00A4A191 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33E20B3026EFCDFC00A4A191 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 686BE82B25E58CCF00862533 /* 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"; + }; + 3B2E8279C112D7129C8D23F1 /* [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"; + }; + C6989ECD8FF0836301D734B4 /* [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 */ + 33E20B2E26EFCDFC00A4A191 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 686BE82925E58CCF00862533 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 686BE83025E58CCF00862533 /* RunnerUITests.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 */ + 33E20B3826EFCDFC00A4A191 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 33E20B3726EFCDFC00A4A191 /* PBXContainerItemProxy */; + }; + 686BE83325E58CCF00862533 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 686BE83225E58CCF00862533 /* 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 */ + 33E20B3926EFCDFC00A4A191 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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; + }; + 33E20B3A26EFCDFC00A4A191 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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; + }; + 686BE83425E58CCF00862533 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 686BE83525E58CCF00862533 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + 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.quickActionsExample; + 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.quickActionsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33E20B3926EFCDFC00A4A191 /* Debug */, + 33E20B3A26EFCDFC00A4A191 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 686BE83425E58CCF00862533 /* Debug */, + 686BE83525E58CCF00862533 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 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/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..1ba2b47c79f1 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme new file mode 100644 index 000000000000..0164e94407dd --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/sensors/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/sensors/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/quick_actions/quick_actions_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/share/example/ios/Runner/AppDelegate.h b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/share/example/ios/Runner/AppDelegate.h rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.h diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.m b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..a89d86c28c6f --- /dev/null +++ b/packages/quick_actions/quick_actions_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. + [super application:application didFinishLaunchingWithOptions:launchOptions]; + return NO; +} +@end diff --git a/packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/sensors/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/sensors/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/sensors/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/share/example/ios/Runner/Base.lproj/Main.storyboard b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/share/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/quick_actions/quick_actions_ios/example/ios/Runner/Base.lproj/Main.storyboard 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 new file mode 100644 index 000000000000..d6bca84ca23d --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + quick_actions_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/main.m b/packages/quick_actions/quick_actions_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/quick_actions/quick_actions_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/url_launcher/url_launcher/example/ios/RunnerTests/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/url_launcher/url_launcher/example/ios/RunnerTests/Info.plist rename to packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Info.plist 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 new file mode 100644 index 000000000000..4a96d05acb58 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/RunnerTests.m @@ -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 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/url_launcher/url_launcher/example/ios/RunnerUITests/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/url_launcher/url_launcher/example/ios/RunnerUITests/Info.plist rename to packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/Info.plist 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 new file mode 100644 index 000000000000..0bad57f886de --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.m @@ -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. + +#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/lib/main.dart b/packages/quick_actions/quick_actions_ios/example/lib/main.dart new file mode 100644 index 000000000000..008917b724e0 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/lib/main.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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:quick_actions_ios/quick_actions_ios.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Quick Actions Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key}) : super(key: key); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + String shortcut = 'no action set'; + + @override + void initState() { + super.initState(); + + final QuickActionsIos quickActions = QuickActionsIos(); + quickActions.initialize((String shortcutType) { + setState(() { + if (shortcutType != null) { + shortcut = shortcutType; + } + }); + }); + + quickActions.setShortcutItems([ + const ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ), + const ShortcutItem( + type: 'action_two', + localizedTitle: 'Action two', + icon: 'ic_launcher'), + ]).then((void _) { + setState(() { + if (shortcut == 'no action set') { + shortcut = 'actions ready'; + } + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(shortcut), + ), + body: const Center( + child: Text('On home screen, long press the app icon to ' + 'get Action one or Action two options. Tapping on that action should ' + 'set the toolbar title.'), + ), + ); + } +} diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml new file mode 100644 index 000000000000..49c6b5e89ed4 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: quick_actions_example +description: Demonstrates how to use the quick_actions plugin. +publish_to: none + +environment: + sdk: ">=2.15.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + quick_actions_ios: + # When depending on this package from a real application you should use: + # quick_actions_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: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/quick_actions/quick_actions_ios/example/test_driver/integration_test.dart b/packages/quick_actions/quick_actions_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/quick_actions/quick_actions_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/quick_actions/quick_actions_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/quick_actions/quick_actions_ios/ios/Assets/.gitkeep diff --git a/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.h b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.h similarity index 100% rename from packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.h rename to packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.h diff --git a/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.m similarity index 96% rename from packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m rename to packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.m index a099b696387c..883352c2ac1d 100644 --- a/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTQuickActionsPlugin.m @@ -4,7 +4,7 @@ #import "FLTQuickActionsPlugin.h" -static NSString *const CHANNEL_NAME = @"plugins.flutter.io/quick_actions"; +static NSString *const kChannelName = @"plugins.flutter.io/quick_actions_ios"; @interface FLTQuickActionsPlugin () @property(nonatomic, retain) FlutterMethodChannel *channel; @@ -15,7 +15,7 @@ @implementation FLTQuickActionsPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME + [FlutterMethodChannel methodChannelWithName:kChannelName binaryMessenger:[registrar messenger]]; FLTQuickActionsPlugin *instance = [[FLTQuickActionsPlugin alloc] init]; instance.channel = channel; 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 new file mode 100644 index 000000000000..e8485f9b4436 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'quick_actions_ios' + s.version = '0.0.1' + s.summary = 'Flutter Quick Actions' + s.description = <<-DESC +This Flutter plugin allows you to manage and interact with the application's home screen quick actions. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/quick_actions' } + s.documentation_url = 'https://pub.dev/packages/quick_actions' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end diff --git a/packages/quick_actions/quick_actions_ios/lib/quick_actions_ios.dart b/packages/quick_actions/quick_actions_ios/lib/quick_actions_ios.dart new file mode 100644 index 000000000000..d19c9ee371bf --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/lib/quick_actions_ios.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. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; + +export 'package:quick_actions_platform_interface/types/types.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/quick_actions_ios'); + +/// An implementation of [QuickActionsPlatform] for iOS. +class QuickActionsIos extends QuickActionsPlatform { + /// Registers this class as the default instance of [QuickActionsPlatform]. + static void registerWith() { + QuickActionsPlatform.instance = QuickActionsIos(); + } + + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + @override + Future initialize(QuickActionHandler handler) async { + channel.setMethodCallHandler((MethodCall call) async { + assert(call.method == 'launch'); + handler(call.arguments as String); + }); + final String? action = + await channel.invokeMethod('getLaunchAction'); + if (action != null) { + handler(action); + } + } + + @override + Future setShortcutItems(List items) async { + final List> itemsList = + items.map(_serializeItem).toList(); + await channel.invokeMethod('setShortcutItems', itemsList); + } + + @override + Future clearShortcutItems() => + channel.invokeMethod('clearShortcutItems'); + + Map _serializeItem(ShortcutItem item) { + return { + 'type': item.type, + 'localizedTitle': item.localizedTitle, + 'icon': item.icon, + }; + } +} diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml new file mode 100644 index 000000000000..4dc91f4a5fe7 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -0,0 +1,30 @@ +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+11 + +environment: + sdk: ">=2.15.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: quick_actions + platforms: + ios: + pluginClass: FLTQuickActionsPlugin + dartPluginClass: QuickActionsIos + +dependencies: + flutter: + sdk: flutter + quick_actions_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + plugin_platform_interface: ^2.1.2 + \ No newline at end of file diff --git a/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart b/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart new file mode 100644 index 000000000000..36827a5c6d8c --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter Authors. 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:quick_actions_ios/quick_actions_ios.dart'; +import 'package:quick_actions_platform_interface/quick_actions_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$QuickActionsIos', () { + late List log; + + setUp(() { + log = []; + }); + + QuickActionsIos buildQuickActionsPlugin() { + final QuickActionsIos quickActions = QuickActionsIos(); + quickActions.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + return quickActions; + } + + test('registerWith() registers correct instance', () { + QuickActionsIos.registerWith(); + expect(QuickActionsPlatform.instance, isA()); + }); + + group('#initialize', () { + test('passes getLaunchAction on launch method', () { + final QuickActionsIos quickActions = buildQuickActionsPlugin(); + quickActions.initialize((String type) {}); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + ], + ); + }); + + test('initialize', () async { + final QuickActionsIos quickActions = buildQuickActionsPlugin(); + final Completer quickActionsHandler = Completer(); + await quickActions + .initialize((_) => quickActionsHandler.complete(true)); + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + ], + ); + log.clear(); + + expect(quickActionsHandler.future, completion(isTrue)); + }); + }); + + group('#setShortCutItems', () { + test('passes shortcutItem through channel', () { + final QuickActionsIos quickActions = buildQuickActionsPlugin(); + quickActions.initialize((String type) {}); + quickActions.setShortcutItems([ + const ShortcutItem( + type: 'test', localizedTitle: 'title', icon: 'icon.svg') + ]); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + isMethodCall('setShortcutItems', arguments: >[ + { + 'type': 'test', + 'localizedTitle': 'title', + 'icon': 'icon.svg', + } + ]), + ], + ); + }); + + test('setShortcutItems with demo data', () async { + const String type = 'type'; + const String localizedTitle = 'localizedTitle'; + const String icon = 'icon'; + final QuickActionsIos quickActions = buildQuickActionsPlugin(); + await quickActions.setShortcutItems( + const [ + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon) + ], + ); + expect( + log, + [ + isMethodCall( + 'setShortcutItems', + arguments: >[ + { + 'type': type, + 'localizedTitle': localizedTitle, + 'icon': icon, + } + ], + ), + ], + ); + log.clear(); + }); + }); + + group('#clearShortCutItems', () { + test('send clearShortcutItems through channel', () { + final QuickActionsIos quickActions = buildQuickActionsPlugin(); + quickActions.initialize((String type) {}); + quickActions.clearShortcutItems(); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + isMethodCall('clearShortcutItems', arguments: null), + ], + ); + }); + + test('clearShortcutItems', () { + final QuickActionsIos quickActions = buildQuickActionsPlugin(); + quickActions.clearShortcutItems(); + expect( + log, + [ + isMethodCall('clearShortcutItems', arguments: null), + ], + ); + log.clear(); + }); + }); + }); + + group('$ShortcutItem', () { + test('Shortcut item can be constructed', () { + const String type = 'type'; + const String localizedTitle = 'title'; + const String icon = 'foo'; + + const ShortcutItem item = + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon); + + expect(item.type, type); + expect(item.localizedTitle, localizedTitle); + expect(item.icon, icon); + }); + }); +} diff --git a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md index 4b63991e9c4a..ad959de03be8 100644 --- a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ -# 1.0.0 +## 1.0.2 + +* Removes dependency on `meta`. + +## 1.0.1 + +* Updates code for analyzer changes. +* Update to use the `verify` method introduced in plugin_platform_interface 2.1.0. + +## 1.0.0 * Initial release of quick_actions_platform_interface 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 8172fe017a4d..560c199ee77a 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,12 +4,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show visibleForTesting; import 'package:quick_actions_platform_interface/types/types.dart'; import '../platform_interface/quick_actions_platform.dart'; -final MethodChannel _channel = +const MethodChannel _channel = MethodChannel('plugins.flutter.io/quick_actions'); /// An implementation of [QuickActionsPlatform] that uses method channels. @@ -22,7 +21,7 @@ class MethodChannelQuickActions extends QuickActionsPlatform { Future initialize(QuickActionHandler handler) async { channel.setMethodCallHandler((MethodCall call) async { assert(call.method == 'launch'); - handler(call.arguments); + handler(call.arguments as String); }); final String? action = await channel.invokeMethod('getLaunchAction'); 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 2e06935ccb09..7a70bba5c81d 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 @@ -32,7 +32,7 @@ abstract class QuickActionsPlatform extends PlatformInterface { // TODO(amirh): Extract common platform interface logic. // https://github.com/flutter/flutter/issues/43368 static set instance(QuickActionsPlatform instance) { - PlatformInterface.verifyToken(instance, _token); + PlatformInterface.verify(instance, _token); _instance = instance; } @@ -40,16 +40,16 @@ abstract class QuickActionsPlatform extends PlatformInterface { /// /// Call this once before any further interaction with the plugin. Future initialize(QuickActionHandler handler) async { - throw UnimplementedError("initialize() has not been implemented."); + throw UnimplementedError('initialize() has not been implemented.'); } /// Sets the [ShortcutItem]s to become the app's quick actions. Future setShortcutItems(List items) async { - throw UnimplementedError("setShortcutItems() has not been implemented."); + throw UnimplementedError('setShortcutItems() has not been implemented.'); } /// Removes all [ShortcutItem]s registered for the app. Future clearShortcutItems() { - throw UnimplementedError("clearShortcutItems() has not been implemented."); + throw UnimplementedError('clearShortcutItems() has not been implemented.'); } } diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/types/quick_action_handler.dart b/packages/quick_actions/quick_actions_platform_interface/lib/types/quick_action_handler.dart index 27c6bb494dfd..ecc813863369 100644 --- a/packages/quick_actions/quick_actions_platform_interface/lib/types/quick_action_handler.dart +++ b/packages/quick_actions/quick_actions_platform_interface/lib/types/quick_action_handler.dart @@ -5,4 +5,4 @@ /// Handler for a quick action launch event. /// /// The argument [type] corresponds to the [ShortcutItem]'s field. -typedef void QuickActionHandler(String type); +typedef QuickActionHandler = void Function(String type); diff --git a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml index 4b9542eb1649..c465b2aaf99b 100644 --- a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml +++ b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml @@ -1,20 +1,19 @@ name: quick_actions_platform_interface description: A common platform interface for the quick_actions plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions/quick_actions_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_platform_interface 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.0 +version: 1.0.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - meta: ^1.3.0 - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: diff --git a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart index f3e172e207fe..240f11bd8037 100644 --- a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart +++ b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart @@ -13,7 +13,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$MethodChannelQuickActions', () { - MethodChannelQuickActions quickActions = MethodChannelQuickActions(); + final MethodChannelQuickActions quickActions = MethodChannelQuickActions(); final List log = []; @@ -29,9 +29,7 @@ void main() { group('#initialize', () { test('passes getLaunchAction on launch method', () { - quickActions.initialize((type) { - 'launch'; - }); + quickActions.initialize((String type) {}); expect( log, @@ -59,19 +57,18 @@ void main() { group('#setShortCutItems', () { test('passes shortcutItem through channel', () { - quickActions.initialize((type) { - 'launch'; - }); - quickActions.setShortcutItems([ - ShortcutItem(type: 'test', localizedTitle: 'title', icon: 'icon.svg') + quickActions.initialize((String type) {}); + quickActions.setShortcutItems([ + const ShortcutItem( + type: 'test', localizedTitle: 'title', icon: 'icon.svg') ]); expect( log, [ isMethodCall('getLaunchAction', arguments: null), - isMethodCall('setShortcutItems', arguments: [ - { + isMethodCall('setShortcutItems', arguments: >[ + { 'type': 'test', 'localizedTitle': 'title', 'icon': 'icon.svg', @@ -111,9 +108,7 @@ void main() { group('#clearShortCutItems', () { test('send clearShortcutItems through channel', () { - quickActions.initialize((type) { - 'launch'; - }); + quickActions.initialize((String type) {}); quickActions.clearShortcutItems(); expect( 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 8ce40816217f..b9655dc56a3c 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 @@ -5,13 +5,17 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:quick_actions_platform_interface/method_channel/method_channel_quick_actions.dart'; import 'package:quick_actions_platform_interface/platform_interface/quick_actions_platform.dart'; +import 'package:quick_actions_platform_interface/types/types.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final QuickActionsPlatform initialInstance = QuickActionsPlatform.instance; + group('$QuickActionsPlatform', () { test('$MethodChannelQuickActions is the default instance', () { - expect(QuickActionsPlatform.instance, isA()); + expect(initialInstance, isA()); }); test('Cannot be implemented with `implements`', () { @@ -28,11 +32,12 @@ void main() { 'Default implementation of initialize() should throw unimplemented error', () { // Arrange - final QuickActionsPlatform = ExtendsQuickActionsPlatform(); + final ExtendsQuickActionsPlatform quickActionsPlatform = + ExtendsQuickActionsPlatform(); // Act & Assert expect( - () => QuickActionsPlatform.initialize((type) {}), + () => quickActionsPlatform.initialize((String type) {}), throwsUnimplementedError, ); }); @@ -41,11 +46,12 @@ void main() { 'Default implementation of setShortcutItems() should throw unimplemented error', () { // Arrange - final QuickActionsPlatform = ExtendsQuickActionsPlatform(); + final ExtendsQuickActionsPlatform quickActionsPlatform = + ExtendsQuickActionsPlatform(); // Act & Assert expect( - () => QuickActionsPlatform.setShortcutItems([]), + () => quickActionsPlatform.setShortcutItems([]), throwsUnimplementedError, ); }); @@ -54,11 +60,12 @@ void main() { 'Default implementation of clearShortcutItems() should throw unimplemented error', () { // Arrange - final QuickActionsPlatform = ExtendsQuickActionsPlatform(); + final ExtendsQuickActionsPlatform quickActionsPlatform = + ExtendsQuickActionsPlatform(); // Act & Assert expect( - () => QuickActionsPlatform.clearShortcutItems(), + () => quickActionsPlatform.clearShortcutItems(), throwsUnimplementedError, ); }); diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md deleted file mode 100644 index acea470855fb..000000000000 --- a/packages/sensors/CHANGELOG.md +++ /dev/null @@ -1,177 +0,0 @@ -## NEXT - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. - -## 2.0.3 - -* Update README to point to Plus Plugins version. - -## 2.0.2 - -* Fix -Wstrict-prototypes analyzer warning in iOS plugin. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 0.4.2+8 - -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) - -## 0.4.2+7 - -* Update Flutter SDK constraint. - -## 0.4.2+6 - -* Update android compileSdkVersion to 29. - -## 0.4.2+5 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.2+4 - -* Update package:e2e -> package:integration_test - -## 0.4.2+3 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.2+2 - -* Post-v2 Android embedding cleanup. - -## 0.4.2+1 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.2 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. -* Fix CocoaPods podspec lint warnings. - -## 0.4.1+10 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.1+9 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.1+8 - -* Make the pedantic dev_dependency explicit. - -## 0.4.1+7 - -* Fixed example userAccelerometerEvent in documentation - -## 0.4.1+6 - -* Migrate from deprecated BinaryMessages to ServicesBinding.instance.defaultBinaryMessenger. -* Require Flutter SDK 1.12.13+hotfix.5 or greater (current stable). - -## 0.4.1+5 - -* Fix example `setState()` called after `dispose()` by canceling the timer. - -## 0.4.1+4 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.1+3 - -* Improve documentation and add unit test coverage. - -## 0.4.1+2 - -* Remove AndroidX warnings. - -## 0.4.1+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.1 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - -## 0.4.0+3 - -* Update and migrate iOS example project. -* Define clang module for iOS. - -## 0.4.0+2 - -* Suppress deprecation warning for BinaryMessages. See: https://github.com/flutter/flutter/issues/33446 - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.5 - -* Added missing test package dependency. - -## 0.3.4 - -* Make sensors Dart 2 compliant. - -## 0.3.3 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.3.2 - -* Added user acceleration sensor events (i.e. accelerometer without gravity). - -## 0.3.1 - -* Fixed Dart 2 type error with iOS sensor events. - -## 0.3.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.2.1 - -* Fixed warnings from the Dart 2.0 analyzer. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.2.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.1.1 - -* Added FLT prefix to iOS types. - -## 0.1.0 - -* Initial Open Source release. diff --git a/packages/sensors/README.md b/packages/sensors/README.md deleted file mode 100644 index 1f46ce1c3608..000000000000 --- a/packages/sensors/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# sensors - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`sensors_plus`](https://pub.dev/packages/sensors_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -A Flutter plugin to access the accelerometer and gyroscope sensors. - -## Usage - -To use this plugin, add `sensors` as a [dependency in your pubspec.yaml -file](https://flutter.dev/docs/development/platform-integration/platform-channels). - -This will expose three classes of sensor events, through three different -streams. - -- `AccelerometerEvent`s describe the velocity of the device, including the - effects of gravity. Put simply, you can use accelerometer readings to tell if - the device is moving in a particular direction. -- `UserAccelerometerEvent`s also describe the velocity of the device, but don't - include gravity. They can also be thought of as just the user's affect on the - device. -- `GyroscopeEvent`s describe the rotation of the device. - -Each of these is exposed through a `BroadcastStream`: `accelerometerEvents`, -`userAccelerometerEvents`, and `gyroscopeEvents`, respectively. - - -### Example - -``` dart -import 'package:sensors/sensors.dart'; - -accelerometerEvents.listen((AccelerometerEvent event) { - print(event); -}); -// [AccelerometerEvent (x: 0.0, y: 9.8, z: 0.0)] - -userAccelerometerEvents.listen((UserAccelerometerEvent event) { - print(event); -}); -// [UserAccelerometerEvent (x: 0.0, y: 0.0, z: 0.0)] - -gyroscopeEvents.listen((GyroscopeEvent event) { - print(event); -}); -// [GyroscopeEvent (x: 0.0, y: 0.0, z: 0.0)] - -``` - -Also see the `example` subdirectory for an example application that uses the -sensor data. diff --git a/packages/sensors/analysis_options.yaml b/packages/sensors/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/sensors/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle deleted file mode 100644 index 7e1087764dee..000000000000 --- a/packages/sensors/android/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -group 'io.flutter.plugins.sensors' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/sensors/android/settings.gradle b/packages/sensors/android/settings.gradle deleted file mode 100644 index 48202890db16..000000000000 --- a/packages/sensors/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'sensors' diff --git a/packages/sensors/android/src/main/AndroidManifest.xml b/packages/sensors/android/src/main/AndroidManifest.xml deleted file mode 100644 index 44d0c9993ce9..000000000000 --- a/packages/sensors/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/SensorsPlugin.java b/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/SensorsPlugin.java deleted file mode 100644 index c643edce3401..000000000000 --- a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/SensorsPlugin.java +++ /dev/null @@ -1,72 +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.sensors; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorManager; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; - -/** SensorsPlugin */ -public class SensorsPlugin implements FlutterPlugin { - private static final String ACCELEROMETER_CHANNEL_NAME = - "plugins.flutter.io/sensors/accelerometer"; - private static final String GYROSCOPE_CHANNEL_NAME = "plugins.flutter.io/sensors/gyroscope"; - private static final String USER_ACCELEROMETER_CHANNEL_NAME = - "plugins.flutter.io/sensors/user_accel"; - - private EventChannel accelerometerChannel; - private EventChannel userAccelChannel; - private EventChannel gyroscopeChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - SensorsPlugin plugin = new SensorsPlugin(); - plugin.setupEventChannels(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - final Context context = binding.getApplicationContext(); - setupEventChannels(context, binding.getBinaryMessenger()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - teardownEventChannels(); - } - - private void setupEventChannels(Context context, BinaryMessenger messenger) { - accelerometerChannel = new EventChannel(messenger, ACCELEROMETER_CHANNEL_NAME); - final StreamHandlerImpl accelerationStreamHandler = - new StreamHandlerImpl( - (SensorManager) context.getSystemService(context.SENSOR_SERVICE), - Sensor.TYPE_ACCELEROMETER); - accelerometerChannel.setStreamHandler(accelerationStreamHandler); - - userAccelChannel = new EventChannel(messenger, USER_ACCELEROMETER_CHANNEL_NAME); - final StreamHandlerImpl linearAccelerationStreamHandler = - new StreamHandlerImpl( - (SensorManager) context.getSystemService(context.SENSOR_SERVICE), - Sensor.TYPE_LINEAR_ACCELERATION); - userAccelChannel.setStreamHandler(linearAccelerationStreamHandler); - - gyroscopeChannel = new EventChannel(messenger, GYROSCOPE_CHANNEL_NAME); - final StreamHandlerImpl gyroScopeStreamHandler = - new StreamHandlerImpl( - (SensorManager) context.getSystemService(context.SENSOR_SERVICE), - Sensor.TYPE_GYROSCOPE); - gyroscopeChannel.setStreamHandler(gyroScopeStreamHandler); - } - - private void teardownEventChannels() { - accelerometerChannel.setStreamHandler(null); - userAccelChannel.setStreamHandler(null); - gyroscopeChannel.setStreamHandler(null); - } -} diff --git a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java b/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java deleted file mode 100644 index 7e6da156386d..000000000000 --- a/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.sensors; - -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import io.flutter.plugin.common.EventChannel; - -class StreamHandlerImpl implements EventChannel.StreamHandler { - - private SensorEventListener sensorEventListener; - private final SensorManager sensorManager; - private final Sensor sensor; - - StreamHandlerImpl(SensorManager sensorManager, int sensorType) { - this.sensorManager = sensorManager; - sensor = sensorManager.getDefaultSensor(sensorType); - } - - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - sensorEventListener = createSensorEventListener(events); - sensorManager.registerListener(sensorEventListener, sensor, sensorManager.SENSOR_DELAY_NORMAL); - } - - @Override - public void onCancel(Object arguments) { - sensorManager.unregisterListener(sensorEventListener); - } - - SensorEventListener createSensorEventListener(final EventChannel.EventSink events) { - return new SensorEventListener() { - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) {} - - @Override - public void onSensorChanged(SensorEvent event) { - double[] sensorValues = new double[event.values.length]; - for (int i = 0; i < event.values.length; i++) { - sensorValues[i] = event.values[i]; - } - events.success(sensorValues); - } - }; - } -} diff --git a/packages/sensors/example/README.md b/packages/sensors/example/README.md deleted file mode 100644 index 9e7d7e0a76a9..000000000000 --- a/packages/sensors/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# sensors_example - -Demonstrates how to use the sensors plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/sensors/example/android.iml b/packages/sensors/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/sensors/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/sensors/example/android/app/build.gradle b/packages/sensors/example/android/app/build.gradle deleted file mode 100644 index d9c1e41f0759..000000000000 --- a/packages/sensors/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.sensorsexample" - minSdkVersion 16 - 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 - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/sensors/example/android/app/src/main/AndroidManifest.xml b/packages/sensors/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index ea3155cb9722..000000000000 --- a/packages/sensors/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java deleted file mode 100644 index 52a6b8bebaf3..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java +++ /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. - -package io.flutter.plugins.sensorsexample; - -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/sensors/example/android/build.gradle b/packages/sensors/example/android/build.gradle deleted file mode 100644 index e101ac08df55..000000000000 --- a/packages/sensors/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -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/sensors/example/android/gradle.properties b/packages/sensors/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/sensors/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/sensors/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/sensors/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/sensors/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -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/sensors/example/integration_test/sensors_test.dart b/packages/sensors/example/integration_test/sensors_test.dart deleted file mode 100644 index 3b8f614d2dcb..000000000000 --- a/packages/sensors/example/integration_test/sensors_test.dart +++ /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. - -// @dart = 2.9 - -import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sensors/sensors.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can subscript to accelerometerEvents and get non-null events', - (WidgetTester tester) async { - final Completer completer = - Completer(); - StreamSubscription subscription; - subscription = accelerometerEvents.listen((AccelerometerEvent event) { - completer.complete(event); - subscription.cancel(); - }); - expect(await completer.future, isNotNull); - }); -} diff --git a/packages/sensors/example/ios/Flutter/AppFrameworkInfo.plist b/packages/sensors/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/sensors/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/sensors/example/ios/Flutter/Debug.xcconfig b/packages/sensors/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/sensors/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/sensors/example/ios/Flutter/Release.xcconfig b/packages/sensors/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/sensors/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/sensors/example/ios/Podfile b/packages/sensors/example/ios/Podfile deleted file mode 100644 index f7d6a5e68c3a..000000000000 --- a/packages/sensors/example/ios/Podfile +++ /dev/null @@ -1,38 +0,0 @@ -# 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__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj b/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 69cd37f9ab86..000000000000 --- a/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,462 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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 */; }; - A5B646543530B300A487D9B1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B16C8D77F2F0873936309F38 /* libPods-Runner.a */; }; -/* 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 = ""; }; - 518BFCF6A33590E963FE1FA9 /* 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 = ""; }; - 65D7779632A59CFED1723B85 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 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 = ""; }; - B16C8D77F2F0873936309F38 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A5B646543530B300A487D9B1 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 5B101E38E51195F91ACE826E /* Pods */ = { - isa = PBXGroup; - children = ( - 65D7779632A59CFED1723B85 /* Pods-Runner.debug.xcconfig */, - 518BFCF6A33590E963FE1FA9 /* Pods-Runner.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 = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 5B101E38E51195F91ACE826E /* Pods */, - DEA20432CDDA0D695086BE46 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - DEA20432CDDA0D695086BE46 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B16C8D77F2F0873936309F38 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 7B77DB2BA78582CC43C8E79F /* [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 = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 7B77DB2BA78582CC43C8E79F /* [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"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - 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.sensorsExample; - 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.sensorsExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/sensors/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/sensors/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/sensors/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/sensors/example/ios/Runner/Info.plist b/packages/sensors/example/ios/Runner/Info.plist deleted file mode 100644 index bc49e9088995..000000000000 --- a/packages/sensors/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - sensors_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/sensors/example/ios/Runner/main.m b/packages/sensors/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/sensors/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/sensors/example/lib/main.dart b/packages/sensors/example/lib/main.dart deleted file mode 100644 index 0946a8e8421b..000000000000 --- a/packages/sensors/example/lib/main.dart +++ /dev/null @@ -1,144 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:sensors/sensors.dart'; - -import 'snake.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Sensors Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - static const int _snakeRows = 20; - static const int _snakeColumns = 20; - static const double _snakeCellSize = 10.0; - - List? _accelerometerValues; - List? _userAccelerometerValues; - List? _gyroscopeValues; - List> _streamSubscriptions = - >[]; - - @override - Widget build(BuildContext context) { - final List? accelerometer = - _accelerometerValues?.map((double v) => v.toStringAsFixed(1)).toList(); - final List? gyroscope = - _gyroscopeValues?.map((double v) => v.toStringAsFixed(1)).toList(); - final List? userAccelerometer = _userAccelerometerValues - ?.map((double v) => v.toStringAsFixed(1)) - .toList(); - - return Scaffold( - appBar: AppBar( - title: const Text('Sensor Example'), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all(width: 1.0, color: Colors.black38), - ), - child: SizedBox( - height: _snakeRows * _snakeCellSize, - width: _snakeColumns * _snakeCellSize, - child: Snake( - rows: _snakeRows, - columns: _snakeColumns, - cellSize: _snakeCellSize, - ), - ), - ), - ), - Padding( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Accelerometer: $accelerometer'), - ], - ), - padding: const EdgeInsets.all(16.0), - ), - Padding( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('UserAccelerometer: $userAccelerometer'), - ], - ), - padding: const EdgeInsets.all(16.0), - ), - Padding( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Gyroscope: $gyroscope'), - ], - ), - padding: const EdgeInsets.all(16.0), - ), - ], - ), - ); - } - - @override - void dispose() { - super.dispose(); - for (StreamSubscription subscription in _streamSubscriptions) { - subscription.cancel(); - } - } - - @override - void initState() { - super.initState(); - _streamSubscriptions - .add(accelerometerEvents.listen((AccelerometerEvent event) { - setState(() { - _accelerometerValues = [event.x, event.y, event.z]; - }); - })); - _streamSubscriptions.add(gyroscopeEvents.listen((GyroscopeEvent event) { - setState(() { - _gyroscopeValues = [event.x, event.y, event.z]; - }); - })); - _streamSubscriptions - .add(userAccelerometerEvents.listen((UserAccelerometerEvent event) { - setState(() { - _userAccelerometerValues = [event.x, event.y, event.z]; - }); - })); - } -} diff --git a/packages/sensors/example/lib/snake.dart b/packages/sensors/example/lib/snake.dart deleted file mode 100644 index 47177681020f..000000000000 --- a/packages/sensors/example/lib/snake.dart +++ /dev/null @@ -1,129 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:sensors/sensors.dart'; - -class Snake extends StatefulWidget { - Snake({this.rows = 20, this.columns = 20, this.cellSize = 10.0}) { - assert(10 <= rows); - assert(10 <= columns); - assert(5.0 <= cellSize); - } - - final int rows; - final int columns; - final double cellSize; - - @override - State createState() => SnakeState(rows, columns, cellSize); -} - -class SnakeBoardPainter extends CustomPainter { - SnakeBoardPainter(this.state, this.cellSize); - - GameState state; - double cellSize; - - @override - void paint(Canvas canvas, Size size) { - final Paint blackLine = Paint()..color = Colors.black; - final Paint blackFilled = Paint() - ..color = Colors.black - ..style = PaintingStyle.fill; - canvas.drawRect( - Rect.fromPoints(Offset.zero, size.bottomLeft(Offset.zero)), - blackLine, - ); - for (math.Point p in state.body) { - final Offset a = Offset(cellSize * p.x, cellSize * p.y); - final Offset b = Offset(cellSize * (p.x + 1), cellSize * (p.y + 1)); - - canvas.drawRect(Rect.fromPoints(a, b), blackFilled); - } - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} - -class SnakeState extends State { - SnakeState(int rows, int columns, this.cellSize) - : state = GameState(rows, columns); - - double cellSize; - GameState state; - AccelerometerEvent? acceleration; - late StreamSubscription _streamSubscription; - late Timer _timer; - - @override - Widget build(BuildContext context) { - return CustomPaint(painter: SnakeBoardPainter(state, cellSize)); - } - - @override - void dispose() { - super.dispose(); - _streamSubscription.cancel(); - _timer.cancel(); - } - - @override - void initState() { - super.initState(); - _streamSubscription = - accelerometerEvents.listen((AccelerometerEvent event) { - setState(() { - acceleration = event; - }); - }); - - _timer = Timer.periodic(const Duration(milliseconds: 200), (_) { - setState(() { - _step(); - }); - }); - } - - void _step() { - final AccelerometerEvent? currentAcceleration = acceleration; - final math.Point? newDirection = currentAcceleration == null - ? null - : currentAcceleration.x.abs() < 1.0 && currentAcceleration.y.abs() < 1.0 - ? null - : (currentAcceleration.x.abs() < currentAcceleration.y.abs()) - ? math.Point(0, currentAcceleration.y.sign.toInt()) - : math.Point(-currentAcceleration.x.sign.toInt(), 0); - state.step(newDirection); - } -} - -class GameState { - GameState(this.rows, this.columns) - : snakeLength = math.min(rows, columns) - 5; - - int rows; - int columns; - int snakeLength; - - List> body = >[const math.Point(0, 0)]; - math.Point direction = const math.Point(1, 0); - - void step(math.Point? newDirection) { - math.Point next = body.last + direction; - next = math.Point(next.x % columns, next.y % rows); - - body.add(next); - if (body.length > snakeLength) body.removeAt(0); - direction = newDirection ?? direction; - } -} diff --git a/packages/sensors/example/pubspec.yaml b/packages/sensors/example/pubspec.yaml deleted file mode 100644 index fee7bd61f736..000000000000 --- a/packages/sensors/example/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: sensors_example -description: Demonstrates how to use the sensors plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - sensors: - # When depending on this package from a real application you should use: - # sensors: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/sensors/example/sensor_example.iml b/packages/sensors/example/sensor_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/sensors/example/sensor_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/sensors/example/sensor_example_android.iml b/packages/sensors/example/sensor_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/sensors/example/sensor_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/sensors/example/test_driver/integration_test.dart b/packages/sensors/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/sensors/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/sensors/ios/Assets/.gitkeep b/packages/sensors/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/sensors/ios/Classes/FLTSensorsPlugin.h b/packages/sensors/ios/Classes/FLTSensorsPlugin.h deleted file mode 100644 index 8c3176b42a44..000000000000 --- a/packages/sensors/ios/Classes/FLTSensorsPlugin.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 - -@interface FLTSensorsPlugin : NSObject -@end - -@interface FLTUserAccelStreamHandler : NSObject -@end - -@interface FLTAccelerometerStreamHandler : NSObject -@end - -@interface FLTGyroscopeStreamHandler : NSObject -@end diff --git a/packages/sensors/ios/Classes/FLTSensorsPlugin.m b/packages/sensors/ios/Classes/FLTSensorsPlugin.m deleted file mode 100644 index 3d0ce66a2b25..000000000000 --- a/packages/sensors/ios/Classes/FLTSensorsPlugin.m +++ /dev/null @@ -1,114 +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 "FLTSensorsPlugin.h" -#import - -@implementation FLTSensorsPlugin - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTAccelerometerStreamHandler* accelerometerStreamHandler = - [[FLTAccelerometerStreamHandler alloc] init]; - FlutterEventChannel* accelerometerChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/sensors/accelerometer" - binaryMessenger:[registrar messenger]]; - [accelerometerChannel setStreamHandler:accelerometerStreamHandler]; - - FLTUserAccelStreamHandler* userAccelerometerStreamHandler = - [[FLTUserAccelStreamHandler alloc] init]; - FlutterEventChannel* userAccelerometerChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/sensors/user_accel" - binaryMessenger:[registrar messenger]]; - [userAccelerometerChannel setStreamHandler:userAccelerometerStreamHandler]; - - FLTGyroscopeStreamHandler* gyroscopeStreamHandler = [[FLTGyroscopeStreamHandler alloc] init]; - FlutterEventChannel* gyroscopeChannel = - [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/sensors/gyroscope" - binaryMessenger:[registrar messenger]]; - [gyroscopeChannel setStreamHandler:gyroscopeStreamHandler]; -} - -@end - -const double GRAVITY = 9.8; -CMMotionManager* _motionManager; - -void _initMotionManager(void) { - if (!_motionManager) { - _motionManager = [[CMMotionManager alloc] init]; - } -} - -static void sendTriplet(Float64 x, Float64 y, Float64 z, FlutterEventSink sink) { - NSMutableData* event = [NSMutableData dataWithCapacity:3 * sizeof(Float64)]; - [event appendBytes:&x length:sizeof(Float64)]; - [event appendBytes:&y length:sizeof(Float64)]; - [event appendBytes:&z length:sizeof(Float64)]; - sink([FlutterStandardTypedData typedDataWithFloat64:event]); -} - -@implementation FLTAccelerometerStreamHandler - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _initMotionManager(); - [_motionManager - startAccelerometerUpdatesToQueue:[[NSOperationQueue alloc] init] - withHandler:^(CMAccelerometerData* accelerometerData, NSError* error) { - CMAcceleration acceleration = accelerometerData.acceleration; - // Multiply by gravity, and adjust sign values to - // align with Android. - sendTriplet(-acceleration.x * GRAVITY, -acceleration.y * GRAVITY, - -acceleration.z * GRAVITY, eventSink); - }]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [_motionManager stopAccelerometerUpdates]; - return nil; -} - -@end - -@implementation FLTUserAccelStreamHandler - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _initMotionManager(); - [_motionManager - startDeviceMotionUpdatesToQueue:[[NSOperationQueue alloc] init] - withHandler:^(CMDeviceMotion* data, NSError* error) { - CMAcceleration acceleration = data.userAcceleration; - // Multiply by gravity, and adjust sign values to align with Android. - sendTriplet(-acceleration.x * GRAVITY, -acceleration.y * GRAVITY, - -acceleration.z * GRAVITY, eventSink); - }]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [_motionManager stopDeviceMotionUpdates]; - return nil; -} - -@end - -@implementation FLTGyroscopeStreamHandler - -- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { - _initMotionManager(); - [_motionManager - startGyroUpdatesToQueue:[[NSOperationQueue alloc] init] - withHandler:^(CMGyroData* gyroData, NSError* error) { - CMRotationRate rotationRate = gyroData.rotationRate; - sendTriplet(rotationRate.x, rotationRate.y, rotationRate.z, eventSink); - }]; - return nil; -} - -- (FlutterError*)onCancelWithArguments:(id)arguments { - [_motionManager stopGyroUpdates]; - return nil; -} - -@end diff --git a/packages/sensors/ios/sensors.podspec b/packages/sensors/ios/sensors.podspec deleted file mode 100644 index 0f0a4be29b0d..000000000000 --- a/packages/sensors/ios/sensors.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'sensors' - s.version = '0.0.1' - s.summary = 'Flutter Sensors' - s.description = <<-DESC -A Flutter plugin to access the accelerometer and gyroscope sensors. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/sensors' } - s.documentation_url = 'https://pub.dev/packages/sensors' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/sensors/lib/sensors.dart b/packages/sensors/lib/sensors.dart deleted file mode 100644 index 8db29e017ad0..000000000000 --- a/packages/sensors/lib/sensors.dart +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:flutter/services.dart'; - -const EventChannel _accelerometerEventChannel = - EventChannel('plugins.flutter.io/sensors/accelerometer'); - -const EventChannel _userAccelerometerEventChannel = - EventChannel('plugins.flutter.io/sensors/user_accel'); - -const EventChannel _gyroscopeEventChannel = - EventChannel('plugins.flutter.io/sensors/gyroscope'); - -/// Discrete reading from an accelerometer. Accelerometers measure the velocity -/// of the device. Note that these readings include the effects of gravity. Put -/// simply, you can use accelerometer readings to tell if the device is moving in -/// a particular direction. -class AccelerometerEvent { - /// Contructs an instance with the given [x], [y], and [z] values. - AccelerometerEvent(this.x, this.y, this.z); - - /// Acceleration force along the x axis (including gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving to the right and negative mean it is moving to the left. - final double x; - - /// Acceleration force along the y axis (including gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving towards the sky and negative mean it is moving towards - /// the ground. - final double y; - - /// Acceleration force along the z axis (including gravity) measured in m/s^2. - /// - /// This uses a right-handed coordinate system. So when the device is held - /// upright and facing the user, positive values mean the device is moving - /// towards the user and negative mean it is moving away from them. - final double z; - - @override - String toString() => '[AccelerometerEvent (x: $x, y: $y, z: $z)]'; -} - -/// Discrete reading from a gyroscope. Gyroscopes measure the rate or rotation of -/// the device in 3D space. -class GyroscopeEvent { - /// Contructs an instance with the given [x], [y], and [z] values. - GyroscopeEvent(this.x, this.y, this.z); - - /// Rate of rotation around the x axis measured in rad/s. - /// - /// When the device is held upright, this can also be thought of as describing - /// "pitch". The top of the device will tilt towards or away from the - /// user as this value changes. - final double x; - - /// Rate of rotation around the y axis measured in rad/s. - /// - /// When the device is held upright, this can also be thought of as describing - /// "yaw". The lengthwise edge of the device will rotate towards or away from - /// the user as this value changes. - final double y; - - /// Rate of rotation around the z axis measured in rad/s. - /// - /// When the device is held upright, this can also be thought of as describing - /// "roll". When this changes the face of the device should remain facing - /// forward, but the orientation will change from portrait to landscape and so - /// on. - final double z; - - @override - String toString() => '[GyroscopeEvent (x: $x, y: $y, z: $z)]'; -} - -/// Like [AccelerometerEvent], this is a discrete reading from an accelerometer -/// and measures the velocity of the device. However, unlike -/// [AccelerometerEvent], this event does not include the effects of gravity. -class UserAccelerometerEvent { - /// Contructs an instance with the given [x], [y], and [z] values. - UserAccelerometerEvent(this.x, this.y, this.z); - - /// Acceleration force along the x axis (excluding gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving to the right and negative mean it is moving to the left. - final double x; - - /// Acceleration force along the y axis (excluding gravity) measured in m/s^2. - /// - /// When the device is held upright facing the user, positive values mean the - /// device is moving towards the sky and negative mean it is moving towards - /// the ground. - final double y; - - /// Acceleration force along the z axis (excluding gravity) measured in m/s^2. - /// - /// This uses a right-handed coordinate system. So when the device is held - /// upright and facing the user, positive values mean the device is moving - /// towards the user and negative mean it is moving away from them. - final double z; - - @override - String toString() => '[UserAccelerometerEvent (x: $x, y: $y, z: $z)]'; -} - -AccelerometerEvent _listToAccelerometerEvent(List list) { - return AccelerometerEvent(list[0], list[1], list[2]); -} - -UserAccelerometerEvent _listToUserAccelerometerEvent(List list) { - return UserAccelerometerEvent(list[0], list[1], list[2]); -} - -GyroscopeEvent _listToGyroscopeEvent(List list) { - return GyroscopeEvent(list[0], list[1], list[2]); -} - -Stream? _accelerometerEvents; -Stream? _gyroscopeEvents; -Stream? _userAccelerometerEvents; - -/// A broadcast stream of events from the device accelerometer. -Stream get accelerometerEvents { - Stream? accelerometerEvents = _accelerometerEvents; - if (accelerometerEvents == null) { - accelerometerEvents = - _accelerometerEventChannel.receiveBroadcastStream().map( - (dynamic event) => - _listToAccelerometerEvent(event.cast()), - ); - _accelerometerEvents = accelerometerEvents; - } - - return accelerometerEvents; -} - -/// A broadcast stream of events from the device gyroscope. -Stream get gyroscopeEvents { - Stream? gyroscopeEvents = _gyroscopeEvents; - if (gyroscopeEvents == null) { - gyroscopeEvents = _gyroscopeEventChannel.receiveBroadcastStream().map( - (dynamic event) => _listToGyroscopeEvent(event.cast()), - ); - _gyroscopeEvents = gyroscopeEvents; - } - - return gyroscopeEvents; -} - -/// Events from the device accelerometer with gravity removed. -Stream get userAccelerometerEvents { - Stream? userAccelerometerEvents = - _userAccelerometerEvents; - if (userAccelerometerEvents == null) { - userAccelerometerEvents = - _userAccelerometerEventChannel.receiveBroadcastStream().map( - (dynamic event) => - _listToUserAccelerometerEvent(event.cast()), - ); - _userAccelerometerEvents = userAccelerometerEvents; - } - - return userAccelerometerEvents; -} diff --git a/packages/sensors/pubspec.yaml b/packages/sensors/pubspec.yaml deleted file mode 100644 index b26819b64df0..000000000000 --- a/packages/sensors/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: sensors -description: Flutter plugin for accessing the Android and iOS accelerometer and - gyroscope sensors. -repository: https://github.com/flutter/plugins/tree/master/packages/sensors -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+sensors%22 -version: 2.0.3 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.sensors - pluginClass: SensorsPlugin - ios: - pluginClass: FLTSensorsPlugin - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - test: ^1.16.0 - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/sensors/test/sensors_test.dart b/packages/sensors/test/sensors_test.dart deleted file mode 100644 index bce3afe6205b..000000000000 --- a/packages/sensors/test/sensors_test.dart +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sensors/sensors.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - test('$accelerometerEvents are streamed', () async { - const String channelName = 'plugins.flutter.io/sensors/accelerometer'; - const List sensorData = [1.0, 2.0, 3.0]; - _initializeFakeSensorChannel(channelName, sensorData); - - final AccelerometerEvent event = await accelerometerEvents.first; - - expect(event.x, sensorData[0]); - expect(event.y, sensorData[1]); - expect(event.z, sensorData[2]); - }); - - test('$gyroscopeEvents are streamed', () async { - const String channelName = 'plugins.flutter.io/sensors/gyroscope'; - const List sensorData = [3.0, 4.0, 5.0]; - _initializeFakeSensorChannel(channelName, sensorData); - - final GyroscopeEvent event = await gyroscopeEvents.first; - - expect(event.x, sensorData[0]); - expect(event.y, sensorData[1]); - expect(event.z, sensorData[2]); - }); - - test('$userAccelerometerEvents are streamed', () async { - const String channelName = 'plugins.flutter.io/sensors/user_accel'; - const List sensorData = [6.0, 7.0, 8.0]; - _initializeFakeSensorChannel(channelName, sensorData); - - final UserAccelerometerEvent event = await userAccelerometerEvents.first; - - expect(event.x, sensorData[0]); - expect(event.y, sensorData[1]); - expect(event.z, sensorData[2]); - }); -} - -void _initializeFakeSensorChannel(String channelName, List sensorData) { - const StandardMethodCodec standardMethod = StandardMethodCodec(); - - void _emitEvent(ByteData? event) { - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channelName, - event, - (ByteData? reply) {}, - ); - } - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .setMockMessageHandler(channelName, (ByteData? message) async { - final MethodCall methodCall = standardMethod.decodeMethodCall(message); - if (methodCall.method == 'listen') { - _emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); - _emitEvent(null); - return standardMethod.encodeSuccessEnvelope(null); - } else if (methodCall.method == 'cancel') { - return standardMethod.encodeSuccessEnvelope(null); - } else { - fail('Expected listen or cancel'); - } - }); -} - -/// 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/share/CHANGELOG.md b/packages/share/CHANGELOG.md deleted file mode 100644 index c9a468d925a7..000000000000 --- a/packages/share/CHANGELOG.md +++ /dev/null @@ -1,224 +0,0 @@ -## NEXT - -* Remove references to the Android V1 embedding. -* Updated Android lint settings. - -## 2.0.4 - -* Update README to point to Plus Plugins version. - -## 2.0.3 - -* Do not tear down method channel onDetachedFromActivity. - -## 2.0.2 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.1 - -* Migrate unit tests to sound null safety. - -## 2.0.0 - -* Migrate to null safety. -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. -* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) -* Update README with the new documentation urls. - -## 0.6.5+5 - -* Update Flutter SDK constraint. - -## 0.6.5+4 - -* Fix iPad share window not showing when `origin` is null. - -## 0.6.5+3 - -* Replace deprecated `Environment.getExternalStorageDirectory()` call on Android. -* Upgrade to Android Gradle plugin 3.5.0 & target API level 29. - -## 0.6.5+2 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.6.5+1 - -* Avoiding uses unchecked or unsafe Object Type Casting - -## 0.6.5 - -* Added support for sharing files - -## 0.6.4+5 - -* Update package:e2e -> package:integration_test - -## 0.6.4+4 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.6.4+3 - -* Post-v2 Android embedding cleanup. - -## 0.6.4+2 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.6.4+1 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.6.4 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. -* Fix CocoaPods podspec lint warnings. - -## 0.6.3+8 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.6.3+7 - -* Updated gradle version of example. - -## 0.6.3+6 - -* Make the pedantic dev_dependency explicit. - -## 0.6.3+5 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.6.3+4 - -* Fix pedantic lints. This shouldn't affect existing functionality. - -## 0.6.3+3 - -* README update. - -## 0.6.3+2 - -* Remove AndroidX warnings. - -## 0.6.3+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.6.3 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - -## 0.6.2+4 - -* Define clang module for iOS. - -## 0.6.2+3 - -* Fix iOS crash when setting subject to null. - -## 0.6.2+2 - -* Update and migrate iOS example project. - -## 0.6.2+1 - -* Specify explicit type for `invokeMethod`. -* Use `const` for `Rect`. -* Updated minimum Flutter SDK to 1.6.0. - -## 0.6.2 - -* Add optional subject to fill email subject in case user selects email app. - -## 0.6.1+2 - -* Update Dart code to conform to current Dart formatter. - -## 0.6.1+1 - -* Fix analyzer warnings about `const Rect` in tests. - -## 0.6.1 - -* Updated Android compileSdkVersion to 28 to match other plugins. - -## 0.6.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.6.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.5.3 - -* Added missing test package dependency. -* Bumped version of mockito package dependency to pick up Dart 2 support. - -## 0.5.2 - -* Fixes iOS sharing - -## 0.5.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.5.0 - -* **Breaking change**. Namespaced the `share` method inside a `Share` class. -* Fixed crash when sharing on iPad. -* Added functionality to specify share sheet origin on iOS. - -## 0.4.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.3.2 - -* Fixed Dart 2 type error. - -## 0.3.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.3.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.2.2 - -* Added FLT prefix to iOS types - -## 0.2.1 - -* Updated README -* Bumped buildToolsVersion to 25.0.3 - -## 0.2.0 - -* Upgrade to new plugin registration. (https://groups.google.com/forum/#!topic/flutter-dev/zba1Ynf2OKM) - -## 0.1.0 - -* Initial Open Source release. diff --git a/packages/share/README.md b/packages/share/README.md deleted file mode 100644 index 7fda1198f503..000000000000 --- a/packages/share/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Share plugin - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`share_plus`](https://pub.dev/packages/share_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -[![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dev/packages/share) - -A Flutter plugin to share content from your Flutter app via the platform's -share dialog. - -Wraps the ACTION_SEND Intent on Android and UIActivityViewController -on iOS. - -## Usage - -To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages/). - -## Example - -Import the library. - -``` dart -import 'package:share/share.dart'; -``` - -Then invoke the static `share` method anywhere in your Dart code. - -``` dart -Share.share('check out my website https://example.com'); -``` - -The `share` method also takes an optional `subject` that will be used when -sharing to email. - -``` dart -Share.share('check out my website https://example.com', subject: 'Look what I made!'); -``` - -To share one or multiple files invoke the static `shareFiles` method anywhere in your Dart code. Optionally you can also pass in `text` and `subject`. -``` dart -Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture'); -Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']); -``` diff --git a/packages/share/analysis_options.yaml b/packages/share/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/share/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle deleted file mode 100644 index b2ea363a3e11..000000000000 --- a/packages/share/android/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -group 'io.flutter.plugins.share' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - dependencies { - implementation 'androidx.core:core:1.3.1' - implementation 'androidx.annotation:annotation:1.1.0' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/share/android/settings.gradle b/packages/share/android/settings.gradle deleted file mode 100644 index 64350ae697e7..000000000000 --- a/packages/share/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'share' diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml deleted file mode 100644 index c141a5c67928..000000000000 --- a/packages/share/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java b/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java deleted file mode 100644 index 7f162e883c32..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.share; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.io.*; -import java.util.List; -import java.util.Map; - -/** Handles the method calls for the plugin. */ -class MethodCallHandler implements MethodChannel.MethodCallHandler { - - private Share share; - - MethodCallHandler(Share share) { - this.share = share; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "share": - expectMapArguments(call); - // Android does not support showing the share sheet at a particular point on screen. - String text = call.argument("text"); - String subject = call.argument("subject"); - share.share(text, subject); - result.success(null); - break; - case "shareFiles": - expectMapArguments(call); - - List paths = call.argument("paths"); - List mimeTypes = call.argument("mimeTypes"); - text = call.argument("text"); - subject = call.argument("subject"); - // Android does not support showing the share sheet at a particular point on screen. - try { - share.shareFiles(paths, mimeTypes, text, subject); - result.success(null); - } catch (IOException e) { - result.error(e.getMessage(), null, null); - } - break; - default: - result.notImplemented(); - break; - } - } - - private void expectMapArguments(MethodCall call) throws IllegalArgumentException { - if (!(call.arguments instanceof Map)) { - throw new IllegalArgumentException("Map argument expected"); - } - } -} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java deleted file mode 100644 index fced7bb7f87c..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java +++ /dev/null @@ -1,232 +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.share; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -/** Handles share intent. */ -class Share { - - private Context context; - private Activity activity; - - /** - * Constructs a Share object. The {@code context} and {@code activity} are used to start the share - * intent. The {@code activity} might be null when constructing the {@link Share} object and set - * to non-null when an activity is available using {@link #setActivity(Activity)}. - */ - Share(Context context, Activity activity) { - this.context = context; - this.activity = activity; - } - - /** - * Sets the activity when an activity is available. When the activity becomes unavailable, use - * this method to set it to null. - */ - void setActivity(Activity activity) { - this.activity = activity; - } - - void share(String text, String subject) { - if (text == null || text.isEmpty()) { - throw new IllegalArgumentException("Non-empty text expected"); - } - - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, text); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - shareIntent.setType("text/plain"); - Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); - startActivity(chooserIntent); - } - - void shareFiles(List paths, List mimeTypes, String text, String subject) - throws IOException { - if (paths == null || paths.isEmpty()) { - throw new IllegalArgumentException("Non-empty path expected"); - } - - clearExternalShareFolder(); - ArrayList fileUris = getUrisForPaths(paths); - - Intent shareIntent = new Intent(); - if (fileUris.isEmpty()) { - share(text, subject); - return; - } else if (fileUris.size() == 1) { - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0)); - shareIntent.setType( - !mimeTypes.isEmpty() && mimeTypes.get(0) != null ? mimeTypes.get(0) : "*/*"); - } else { - shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); - shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris); - shareIntent.setType(reduceMimeTypes(mimeTypes)); - } - if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text); - if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); - - List resInfoList = - getContext() - .getPackageManager() - .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - for (Uri fileUri : fileUris) { - getContext() - .grantUriPermission( - packageName, - fileUri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - - startActivity(chooserIntent); - } - - private void startActivity(Intent intent) { - if (activity != null) { - activity.startActivity(intent); - } else if (context != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } else { - throw new IllegalStateException("Both context and activity are null"); - } - } - - private ArrayList getUrisForPaths(List paths) throws IOException { - ArrayList uris = new ArrayList<>(paths.size()); - for (String path : paths) { - File file = new File(path); - if (!fileIsOnExternal(file)) { - file = copyToExternalShareFolder(file); - } - - uris.add( - FileProvider.getUriForFile( - getContext(), getContext().getPackageName() + ".flutter.share_provider", file)); - } - return uris; - } - - private String reduceMimeTypes(List mimeTypes) { - if (mimeTypes.size() > 1) { - String reducedMimeType = mimeTypes.get(0); - for (int i = 1; i < mimeTypes.size(); i++) { - String mimeType = mimeTypes.get(i); - if (!reducedMimeType.equals(mimeType)) { - if (getMimeTypeBase(mimeType).equals(getMimeTypeBase(reducedMimeType))) { - reducedMimeType = getMimeTypeBase(mimeType) + "/*"; - } else { - reducedMimeType = "*/*"; - break; - } - } - } - return reducedMimeType; - } else if (mimeTypes.size() == 1) { - return mimeTypes.get(0); - } else { - return "*/*"; - } - } - - @NonNull - private String getMimeTypeBase(String mimeType) { - if (mimeType == null || !mimeType.contains("/")) { - return "*"; - } - - return mimeType.substring(0, mimeType.indexOf("/")); - } - - private boolean fileIsOnExternal(File file) { - try { - String filePath = file.getCanonicalPath(); - File externalDir = context.getExternalFilesDir(null); - return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath()); - } catch (IOException e) { - return false; - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - private void clearExternalShareFolder() { - File folder = getExternalShareFolder(); - if (folder.exists()) { - for (File file : folder.listFiles()) { - file.delete(); - } - folder.delete(); - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - private File copyToExternalShareFolder(File file) throws IOException { - File folder = getExternalShareFolder(); - if (!folder.exists()) { - folder.mkdirs(); - } - - File newFile = new File(folder, file.getName()); - copy(file, newFile); - return newFile; - } - - @NonNull - private File getExternalShareFolder() { - return new File(getContext().getExternalCacheDir(), "share"); - } - - private Context getContext() { - if (activity != null) { - return activity; - } - if (context != null) { - return context; - } - - throw new IllegalStateException("Both context and activity are null"); - } - - private static void copy(File src, File dst) throws IOException { - InputStream in = new FileInputStream(src); - try { - OutputStream out = new FileOutputStream(dst); - try { - // Transfer bytes from in to out - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } finally { - out.close(); - } - } finally { - in.close(); - } - } -} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java deleted file mode 100644 index fff48a6bad14..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java +++ /dev/null @@ -1,14 +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.share; - -import androidx.core.content.FileProvider; - -/** - * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. - * - *

See https://developer.android.com/guide/topics/manifest/provider-element.html for details. - */ -public class ShareFileProvider extends FileProvider {} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java deleted file mode 100644 index c596b8b71555..000000000000 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.share; - -import android.app.Activity; -import android.content.Context; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; - -/** Plugin method host for presenting a share sheet via Intent */ -public class SharePlugin implements FlutterPlugin, ActivityAware { - - private static final String CHANNEL = "plugins.flutter.io/share"; - private MethodCallHandler handler; - private Share share; - private MethodChannel methodChannel; - - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - SharePlugin plugin = new SharePlugin(); - plugin.setUpChannel(registrar.context(), registrar.activity(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - share = null; - } - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - share.setActivity(binding.getActivity()); - } - - @Override - public void onDetachedFromActivity() { - share.setActivity(null); - } - - @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - onAttachedToActivity(binding); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity(); - } - - private void setUpChannel(Context context, Activity activity, BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, CHANNEL); - share = new Share(context, activity); - handler = new MethodCallHandler(share); - methodChannel.setMethodCallHandler(handler); - } -} diff --git a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml deleted file mode 100644 index e68bf916a30b..000000000000 --- a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/share/example/README.md b/packages/share/example/README.md deleted file mode 100644 index 4081c8a5c9c3..000000000000 --- a/packages/share/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# share_example - -Demonstrates how to use the share plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.dev/). diff --git a/packages/share/example/android.iml b/packages/share/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/share/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/share/example/android/app/build.gradle b/packages/share/example/android/app/build.gradle deleted file mode 100644 index 5b7b30bbad26..000000000000 --- a/packages/share/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.shareexample" - minSdkVersion 16 - targetSdkVersion 29 - 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.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/share/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/share/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/share/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/share/example/android/app/src/main/AndroidManifest.xml b/packages/share/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index d1f1ce953e3a..000000000000 --- a/packages/share/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java deleted file mode 100644 index aba658887d88..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java +++ /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. - -package io.flutter.plugins.shareexample; - -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/share/example/android/build.gradle b/packages/share/example/android/build.gradle deleted file mode 100644 index 456d020f6e2c..000000000000 --- a/packages/share/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -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/share/example/android/gradle.properties b/packages/share/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/share/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/share/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/share/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d757f3d33fcc..000000000000 --- a/packages/share/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/share/example/integration_test/share_test.dart b/packages/share/example/integration_test/share_test.dart deleted file mode 100644 index 54d553bbb5a0..000000000000 --- a/packages/share/example/integration_test/share_test.dart +++ /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. - -// @dart = 2.9 - -import 'package:flutter_test/flutter_test.dart'; -import 'package:share/share.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can launch share', (WidgetTester tester) async { - expect(Share.share('message', subject: 'title'), completes); - }); -} diff --git a/packages/share/example/ios/Flutter/AppFrameworkInfo.plist b/packages/share/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/share/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/share/example/ios/Flutter/Debug.xcconfig b/packages/share/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/share/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/share/example/ios/Flutter/Release.xcconfig b/packages/share/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/share/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index d7e896212533..000000000000 --- a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,698 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 85392794417D70A970945C83 /* libPods-Runner.a */; }; - 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */; }; - 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B4226EFCEF400A4A191 /* RunnerTests.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 683426AD2538D314009B194C /* FLTShareExampleUITests.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 */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - 683426B02538D314009B194C /* 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 */ - 002F2AAB9479773692FEF066 /* 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 = ""; }; - 1BCE6CBBA2E91FD0397A29C8 /* 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 = ""; }; - 2D92224F1EC45DE6007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 33E20B4226EFCEF400A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; - 33E20B4426EFCEF400A4A191 /* 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 = ""; }; - 683426AB2538D314009B194C /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 683426AD2538D314009B194C /* FLTShareExampleUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTShareExampleUITests.m; sourceTree = ""; }; - 683426AF2538D314009B194C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 85392794417D70A970945C83 /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33E20B3D26EFCEF400A4A191 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 683426A82538D314009B194C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 16DDF472245BCC3E62219493 /* Pods */ = { - isa = PBXGroup; - children = ( - 1BCE6CBBA2E91FD0397A29C8 /* Pods-Runner.debug.xcconfig */, - 002F2AAB9479773692FEF066 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 33E20B4126EFCEF400A4A191 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 33E20B4226EFCEF400A4A191 /* RunnerTests.m */, - 33E20B4426EFCEF400A4A191 /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 683426AC2538D314009B194C /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - 683426AD2538D314009B194C /* FLTShareExampleUITests.m */, - 683426AF2538D314009B194C /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; - 8CA31EF57239BF20619316D9 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 85392794417D70A970945C83 /* libPods-Runner.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 */, - 683426AC2538D314009B194C /* RunnerUITests */, - 33E20B4126EFCEF400A4A191 /* RunnerTests */, - 97C146EF1CF9000F007C117D /* Products */, - 16DDF472245BCC3E62219493 /* Pods */, - 8CA31EF57239BF20619316D9 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 683426AB2538D314009B194C /* RunnerUITests.xctest */, - 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 2D92224F1EC45DE6007564B0 /* GeneratedPluginRegistrant.h */, - 2D9222501EC45DE6007564B0 /* 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 = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33E20B3F26EFCEF400A4A191 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 33E20B3C26EFCEF400A4A191 /* Sources */, - 33E20B3D26EFCEF400A4A191 /* Frameworks */, - 33E20B3E26EFCEF400A4A191 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 683426AA2538D314009B194C /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - 683426A72538D314009B194C /* Sources */, - 683426A82538D314009B194C /* Frameworks */, - 683426A92538D314009B194C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 683426B12538D314009B194C /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = 683426AB2538D314009B194C /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 5F8AC0B5B699C537B657C107 /* [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 = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; - TargetAttributes = { - 33E20B3F26EFCEF400A4A191 = { - CreatedOnToolsVersion = 12.5; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 683426AA2538D314009B194C = { - CreatedOnToolsVersion = 11.7; - 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 */, - 683426AA2538D314009B194C /* RunnerUITests */, - 33E20B3F26EFCEF400A4A191 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33E20B3E26EFCEF400A4A191 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 683426A92538D314009B194C /* 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"; - }; - 5F8AC0B5B699C537B657C107 /* [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"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33E20B3C26EFCEF400A4A191 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 683426A72538D314009B194C /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */; - }; - 683426B12538D314009B194C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 683426B02538D314009B194C /* 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 */ - 33E20B4726EFCEF400A4A191 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - 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; - }; - 33E20B4826EFCEF400A4A191 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - 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; - }; - 683426B22538D314009B194C /* 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_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.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; - }; - 683426B32538D314009B194C /* 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_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = RunnerUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.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 = 8.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 = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.shareExample; - 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.shareExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33E20B4726EFCEF400A4A191 /* Debug */, - 33E20B4826EFCEF400A4A191 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 683426B22538D314009B194C /* Debug */, - 683426B32538D314009B194C /* 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/share/example/ios/Runner/Info.plist b/packages/share/example/ios/Runner/Info.plist deleted file mode 100644 index 71656105a1fa..000000000000 --- a/packages/share/example/ios/Runner/Info.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - share_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 - - NSPhotoLibraryUsageDescription - This app requires access to the photo library for sharing images. - NSMicrophoneUsageDescription - This app does not require access to the microphone for sharing images. - NSCameraUsageDescription - This app requires access to the camera for sharing images. - - diff --git a/packages/share/example/ios/Runner/main.m b/packages/share/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/share/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/share/example/ios/RunnerTests/RunnerTests.m b/packages/share/example/ios/RunnerTests/RunnerTests.m deleted file mode 100644 index 3c4c341fd451..000000000000 --- a/packages/share/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 share; -@import XCTest; - -@interface ShareTests : XCTestCase -@end - -@implementation ShareTests - -- (void)testPlugin { - FLTSharePlugin* plugin = [[FLTSharePlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end diff --git a/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m b/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m deleted file mode 100644 index c099cb946b92..000000000000 --- a/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m +++ /dev/null @@ -1,48 +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 NSInteger kSecondsToWaitWhenFindingElements = 30; - -@interface FLTShareExampleUITests : XCTestCase - -@end - -@implementation FLTShareExampleUITests - -- (void)setUp { - self.continueAfterFailure = NO; -} - -- (void)testShareWithEmptyOrigin { - XCUIApplication* app = [[XCUIApplication alloc] init]; - [app launch]; - - XCUIElement* shareWithEmptyOriginButton = [app.buttons - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"label == %@", @"Share With Empty Origin"]]; - if (![shareWithEmptyOriginButton waitForExistenceWithTimeout:kSecondsToWaitWhenFindingElements]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find shareWithEmptyOriginButton with %@ seconds", - @(kSecondsToWaitWhenFindingElements)); - } - - XCTAssertNotNil(shareWithEmptyOriginButton); - [shareWithEmptyOriginButton tap]; - - // Find the share popup. - XCUIElement* activityListView = [app.otherElements - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"identifier == %@", @"ActivityListView"]]; - if (![activityListView waitForExistenceWithTimeout:kSecondsToWaitWhenFindingElements]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find activityListView with %@ seconds", - @(kSecondsToWaitWhenFindingElements)); - } - XCTAssertNotNil(activityListView); -} - -@end diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart deleted file mode 100644 index 9b5b807c77c6..000000000000 --- a/packages/share/example/lib/image_previews.dart +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -/// Widget for displaying a preview of images -class ImagePreviews extends StatelessWidget { - /// The image paths of the displayed images - final List imagePaths; - - /// Callback when an image should be removed - final Function(int)? onDelete; - - /// Creates a widget for preview of images. [imagePaths] can not be empty - /// and all contained paths need to be non empty. - const ImagePreviews(this.imagePaths, {Key? key, this.onDelete}) - : super(key: key); - - @override - Widget build(BuildContext context) { - if (imagePaths.isEmpty) { - return Container(); - } - - List imageWidgets = []; - for (int i = 0; i < imagePaths.length; i++) { - imageWidgets.add(_ImagePreview( - imagePaths[i], - onDelete: onDelete != null ? () => onDelete!(i) : null, - )); - } - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: imageWidgets), - ); - } -} - -class _ImagePreview extends StatelessWidget { - final String imagePath; - final VoidCallback? onDelete; - - const _ImagePreview(this.imagePath, {Key? key, this.onDelete}) - : super(key: key); - - @override - Widget build(BuildContext context) { - File imageFile = File(imagePath); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 200, - maxHeight: 200, - ), - child: Image.file(imageFile), - ), - Positioned( - right: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: FloatingActionButton( - backgroundColor: Colors.red, - child: Icon(Icons.delete), - onPressed: onDelete), - ), - ), - ], - ), - ); - } -} diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart deleted file mode 100644 index f802b492df6d..000000000000 --- a/packages/share/example/lib/main.dart +++ /dev/null @@ -1,136 +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. - -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:share/share.dart'; - -import 'image_previews.dart'; - -void main() { - runApp(DemoApp()); -} - -class DemoApp extends StatefulWidget { - @override - DemoAppState createState() => DemoAppState(); -} - -class DemoAppState extends State { - String text = ''; - String subject = ''; - List imagePaths = []; - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Share Plugin Demo', - home: Scaffold( - appBar: AppBar( - title: const Text('Share Plugin Demo'), - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextField( - decoration: const InputDecoration( - labelText: 'Share text:', - hintText: 'Enter some text and/or link to share', - ), - maxLines: 2, - onChanged: (String value) => setState(() { - text = value; - }), - ), - TextField( - decoration: const InputDecoration( - labelText: 'Share subject:', - hintText: 'Enter subject to share (optional)', - ), - maxLines: 2, - onChanged: (String value) => setState(() { - subject = value; - }), - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - ImagePreviews(imagePaths, onDelete: _onDeleteImage), - ListTile( - leading: Icon(Icons.add), - title: Text("Add image"), - onTap: () async { - final imagePicker = ImagePicker(); - final pickedFile = await imagePicker.getImage( - source: ImageSource.gallery, - ); - if (pickedFile != null) { - setState(() { - imagePaths.add(pickedFile.path); - }); - } - }, - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - Builder( - builder: (BuildContext context) { - return ElevatedButton( - child: const Text('Share'), - onPressed: text.isEmpty && imagePaths.isEmpty - ? null - : () => _onShare(context), - ); - }, - ), - const Padding(padding: EdgeInsets.only(top: 12.0)), - Builder( - builder: (BuildContext context) { - return ElevatedButton( - child: const Text('Share With Empty Origin'), - onPressed: () => _onShareWithEmptyOrigin(context), - ); - }, - ), - ], - ), - ), - )), - ); - } - - _onDeleteImage(int position) { - setState(() { - imagePaths.removeAt(position); - }); - } - - _onShare(BuildContext context) async { - // A builder is used to retrieve the context immediately - // surrounding the ElevatedButton. - // - // The context's `findRenderObject` returns the first - // RenderObject in its descendent tree when it's not - // a RenderObjectWidget. The ElevatedButton's RenderObject - // has its position and size after it's built. - final RenderBox box = context.findRenderObject() as RenderBox; - - if (imagePaths.isNotEmpty) { - await Share.shareFiles(imagePaths, - text: text, - subject: subject, - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); - } else { - await Share.share(text, - subject: subject, - sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); - } - } - - _onShareWithEmptyOrigin(BuildContext context) async { - await Share.share("text"); - } -} diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml deleted file mode 100644 index 8a28b43d46e4..000000000000 --- a/packages/share/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: share_example -description: Demonstrates how to use the share plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2" - -dependencies: - flutter: - sdk: flutter - share: - # When depending on this package from a real application you should use: - # share: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - image_picker: ^0.7.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 - -flutter: - uses-material-design: true diff --git a/packages/share/example/share_example.iml b/packages/share/example/share_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/share/example/share_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/share/example/test_driver/integration_test.dart b/packages/share/example/test_driver/integration_test.dart deleted file mode 100644 index 6a0e6fa82dbe..000000000000 --- a/packages/share/example/test_driver/integration_test.dart +++ /dev/null @@ -1,9 +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:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/share/ios/Assets/.gitkeep b/packages/share/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/share/ios/Classes/FLTSharePlugin.h b/packages/share/ios/Classes/FLTSharePlugin.h deleted file mode 100644 index 8f6a6a538e2a..000000000000 --- a/packages/share/ios/Classes/FLTSharePlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTSharePlugin : NSObject -@end diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m deleted file mode 100644 index b8a3a7ffa316..000000000000 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ /dev/null @@ -1,216 +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 "FLTSharePlugin.h" - -static NSString *const PLATFORM_CHANNEL = @"plugins.flutter.io/share"; - -@interface ShareData : NSObject - -@property(readonly, nonatomic, copy) NSString *subject; -@property(readonly, nonatomic, copy) NSString *text; -@property(readonly, nonatomic, copy) NSString *path; -@property(readonly, nonatomic, copy) NSString *mimeType; - -- (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text NS_DESIGNATED_INITIALIZER; -- (instancetype)initWithFile:(NSString *)path - mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER; - -- (instancetype)init __attribute__((unavailable("Use initWithSubject:text: instead"))); - -@end - -@implementation ShareData - -- (instancetype)init { - [super doesNotRecognizeSelector:_cmd]; - return nil; -} - -- (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text { - self = [super init]; - if (self) { - _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject; - _text = text; - } - return self; -} - -- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType { - self = [super init]; - if (self) { - _path = path; - _mimeType = mimeType; - } - return self; -} - -- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { - return @""; -} - -- (id)activityViewController:(UIActivityViewController *)activityViewController - itemForActivityType:(UIActivityType)activityType { - if (!_path || !_mimeType) { - return _text; - } - - if ([_mimeType hasPrefix:@"image/"]) { - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return image; - } else { - NSURL *url = [NSURL fileURLWithPath:_path]; - return url; - } -} - -- (NSString *)activityViewController:(UIActivityViewController *)activityViewController - subjectForActivityType:(UIActivityType)activityType { - return _subject; -} - -- (UIImage *)activityViewController:(UIActivityViewController *)activityViewController - thumbnailImageForActivityType:(UIActivityType)activityType - suggestedSize:(CGSize)suggestedSize { - if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) { - return nil; - } - - UIImage *image = [UIImage imageWithContentsOfFile:_path]; - return [self imageWithImage:image scaledToSize:suggestedSize]; -} - -- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { - UIGraphicsBeginImageContext(newSize); - [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; - UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return newImage; -} - -@end - -@implementation FLTSharePlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *shareChannel = - [FlutterMethodChannel methodChannelWithName:PLATFORM_CHANNEL - binaryMessenger:registrar.messenger]; - - [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - NSDictionary *arguments = [call arguments]; - NSNumber *originX = arguments[@"originX"]; - NSNumber *originY = arguments[@"originY"]; - NSNumber *originWidth = arguments[@"originWidth"]; - NSNumber *originHeight = arguments[@"originHeight"]; - - CGRect originRect = CGRectZero; - if (originX && originY && originWidth && originHeight) { - originRect = CGRectMake([originX doubleValue], [originY doubleValue], - [originWidth doubleValue], [originHeight doubleValue]); - } - - if ([@"share" isEqualToString:call.method]) { - NSString *shareText = arguments[@"text"]; - NSString *shareSubject = arguments[@"subject"]; - - if (shareText.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty text expected" - details:nil]); - return; - } - - [self shareText:shareText - subject:shareSubject - withController:[UIApplication sharedApplication].keyWindow.rootViewController - atSource:originRect]; - result(nil); - } else if ([@"shareFiles" isEqualToString:call.method]) { - NSArray *paths = arguments[@"paths"]; - NSArray *mimeTypes = arguments[@"mimeTypes"]; - NSString *subject = arguments[@"subject"]; - NSString *text = arguments[@"text"]; - - if (paths.count == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Non-empty paths expected" - details:nil]); - return; - } - - for (NSString *path in paths) { - if (path.length == 0) { - result([FlutterError errorWithCode:@"error" - message:@"Each path must not be empty" - details:nil]); - return; - } - } - - [self shareFiles:paths - withMimeType:mimeTypes - withSubject:subject - withText:text - withController:[UIApplication sharedApplication].keyWindow.rootViewController - atSource:originRect]; - result(nil); - } else { - result(FlutterMethodNotImplemented); - } - }]; -} - -+ (void)share:(NSArray *)shareItems - withController:(UIViewController *)controller - atSource:(CGRect)origin { - UIActivityViewController *activityViewController = - [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; - activityViewController.popoverPresentationController.sourceView = controller.view; - activityViewController.popoverPresentationController.sourceRect = origin; - - [controller presentViewController:activityViewController animated:YES completion:nil]; -} - -+ (void)shareText:(NSString *)shareText - subject:(NSString *)subject - withController:(UIViewController *)controller - atSource:(CGRect)origin { - ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText]; - [self share:@[ data ] withController:controller atSource:origin]; -} - -+ (void)shareFiles:(NSArray *)paths - withMimeType:(NSArray *)mimeTypes - withSubject:(NSString *)subject - withText:(NSString *)text - withController:(UIViewController *)controller - atSource:(CGRect)origin { - NSMutableArray *items = [[NSMutableArray alloc] init]; - - if (text || subject) { - [items addObject:[[ShareData alloc] initWithSubject:subject text:text]]; - } - - for (int i = 0; i < [paths count]; i++) { - NSString *path = paths[i]; - NSString *pathExtension = [path pathExtension]; - NSString *mimeType = mimeTypes[i]; - if ([pathExtension.lowercaseString isEqualToString:@"jpg"] || - [pathExtension.lowercaseString isEqualToString:@"jpeg"] || - [pathExtension.lowercaseString isEqualToString:@"png"] || - [mimeType.lowercaseString isEqualToString:@"image/jpg"] || - [mimeType.lowercaseString isEqualToString:@"image/jpeg"] || - [mimeType.lowercaseString isEqualToString:@"image/png"]) { - UIImage *image = [UIImage imageWithContentsOfFile:path]; - [items addObject:image]; - } else { - [items addObject:[[ShareData alloc] initWithFile:path mimeType:mimeType]]; - } - } - - [self share:items withController:controller atSource:origin]; -} - -@end diff --git a/packages/share/ios/share.podspec b/packages/share/ios/share.podspec deleted file mode 100644 index 786e1c7fb922..000000000000 --- a/packages/share/ios/share.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'share' - s.version = '0.0.1' - s.summary = 'Flutter Share' - s.description = <<-DESC -A Flutter plugin to share content from your Flutter app via the platform's share dialog. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/share' } - s.documentation_url = 'https://pub.dev/packages/share' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart deleted file mode 100644 index 6a3f5a317e31..000000000000 --- a/packages/share/lib/share.dart +++ /dev/null @@ -1,101 +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:meta/meta.dart' show visibleForTesting; -import 'package:mime/mime.dart' show lookupMimeType; - -/// Plugin for summoning a platform share sheet. -class Share { - /// [MethodChannel] used to communicate with the platform side. - @visibleForTesting - static const MethodChannel channel = - MethodChannel('plugins.flutter.io/share'); - - /// Summons the platform's share sheet to share text. - /// - /// Wraps the platform's native share dialog. Can share a text and/or a URL. - /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` - /// on iOS. - /// - /// The optional [subject] parameter can be used to populate a subject if the - /// user chooses to send an email. - /// - /// The optional [sharePositionOrigin] parameter can be used to specify a global - /// origin rect for the share sheet to popover from on iPads. It has no effect - /// on non-iPads. - /// - /// May throw [PlatformException] or [FormatException] - /// from [MethodChannel]. - static Future share( - String text, { - String? subject, - Rect? sharePositionOrigin, - }) { - assert(text != null); - assert(text.isNotEmpty); - final Map params = { - 'text': text, - 'subject': subject, - }; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - return channel.invokeMethod('share', params); - } - - /// Summons the platform's share sheet to share multiple files. - /// - /// Wraps the platform's native share dialog. Can share a file. - /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` - /// on iOS. - /// - /// The optional `sharePositionOrigin` parameter can be used to specify a global - /// origin rect for the share sheet to popover from on iPads. It has no effect - /// on non-iPads. - /// - /// May throw [PlatformException] or [FormatException] - /// from [MethodChannel]. - static Future shareFiles( - List paths, { - List? mimeTypes, - String? subject, - String? text, - Rect? sharePositionOrigin, - }) { - assert(paths != null); - assert(paths.isNotEmpty); - assert(paths.every((element) => element != null && element.isNotEmpty)); - final Map params = { - 'paths': paths, - 'mimeTypes': mimeTypes ?? - paths.map((String path) => _mimeTypeForPath(path)).toList(), - }; - - if (subject != null) params['subject'] = subject; - if (text != null) params['text'] = text; - - if (sharePositionOrigin != null) { - params['originX'] = sharePositionOrigin.left; - params['originY'] = sharePositionOrigin.top; - params['originWidth'] = sharePositionOrigin.width; - params['originHeight'] = sharePositionOrigin.height; - } - - return channel.invokeMethod('shareFiles', params); - } - - static String _mimeTypeForPath(String path) { - assert(path != null); - return lookupMimeType(path) ?? 'application/octet-stream'; - } -} diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml deleted file mode 100644 index 4735995fff8a..000000000000 --- a/packages/share/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: share -description: Flutter plugin for sharing content via the platform share UI, using - the ACTION_SEND intent on Android and UIActivityViewController on iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/share -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+share%22 -version: 2.0.4 - -environment: - flutter: ">=1.12.13+hotfix.5" - sdk: ">=2.12.0 <3.0.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.share - pluginClass: SharePlugin - ios: - pluginClass: FLTSharePlugin - -dependencies: - meta: ^1.3.0 - mime: ^1.0.0 - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart deleted file mode 100644 index d0049cef94ab..000000000000 --- a/packages/share/test/share_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -import 'dart:ui'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:share/share.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); // Required for MethodChannels - - late FakeMethodChannel fakeChannel; - - setUp(() { - fakeChannel = FakeMethodChannel(); - // Re-pipe to our fake to verify invocations. - Share.channel.setMockMethodCallHandler((MethodCall call) async { - // The explicit type can be void as the only method call has a return type of void. - await fakeChannel.invokeMethod(call.method, call.arguments); - }); - }); - - test('sharing empty fails', () { - expect( - () => Share.share(''), - throwsA(isA()), - ); - expect(fakeChannel.invocation, isNull); - }); - - test('sharing origin sets the right params', () async { - await Share.share( - 'some text to share', - subject: 'some subject to share', - sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), - ); - - expect( - fakeChannel.invocation, - equals({ - 'share': { - 'text': 'some text to share', - 'subject': 'some subject to share', - 'originX': 1.0, - 'originY': 2.0, - 'originWidth': 3.0, - 'originHeight': 4.0, - } - }), - ); - }); - - test('sharing empty file fails', () { - expect( - () => Share.shareFiles(['']), - throwsA(isA()), - ); - expect(fakeChannel.invocation, isNull); - }); - - test('sharing file sets correct mimeType', () async { - final String path = 'tempfile-83649a.png'; - final File file = File(path); - try { - file.createSync(); - - await Share.shareFiles([path]); - - expect( - fakeChannel.invocation, - equals({ - 'shareFiles': { - 'paths': [path], - 'mimeTypes': ['image/png'], - } - }), - ); - } finally { - file.deleteSync(); - } - }); - - test('sharing file sets passed mimeType', () async { - final String path = 'tempfile-83649a.png'; - final File file = File(path); - try { - file.createSync(); - - await Share.shareFiles([path], mimeTypes: ['*/*']); - - expect( - fakeChannel.invocation, - equals({ - 'shareFiles': { - 'paths': [file.path], - 'mimeTypes': ['*/*'], - } - }), - ); - } finally { - file.deleteSync(); - } - }); -} - -class FakeMethodChannel extends Fake implements MethodChannel { - Map? invocation; - - @override - Future invokeMethod(String method, [dynamic arguments]) async { - this.invocation = {method: arguments}; - return null; - } -} diff --git a/packages/shared_preferences/analysis_options.yaml b/packages/shared_preferences/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/shared_preferences/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index db12fd1829aa..4bf0f6a6b144 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,35 @@ +## 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. + +## 2.0.12 + +* Removes dependency on `meta`. + +## 2.0.11 + +* Corrects example for mocking in readme. + +## 2.0.10 + +* Removes obsolete manual registration of Windows and Linux implementations. + +## 2.0.9 + +* Fixes newly enabled analyzer options. +* Updates example app Android compileSdkVersion to 31. +* Moved Android and iOS implementations to federated packages. + ## 2.0.8 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index e51ddea1c890..03975ff021e6 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -3,38 +3,58 @@ [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) Wraps platform-specific persistent storage for simple data -(NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, +(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). -### Example - -``` dart -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - runApp(MaterialApp( - home: Scaffold( - body: Center( - child: RaisedButton( - onPressed: _incrementCounter, - child: Text('Increment Counter'), - ), - ), - ), - )); -} - -_incrementCounter() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - int counter = (prefs.getInt('counter') ?? 0) + 1; - print('Pressed $counter times.'); - await prefs.setInt('counter', counter); -} +### Examples +Here are small examples that show you how to use the API. + +#### Write data +```dart +// Obtain shared preferences. +final prefs = await SharedPreferences.getInstance(); + +// Save an integer value to 'counter' key. +await prefs.setInt('counter', 10); +// Save an boolean value to 'repeat' key. +await prefs.setBool('repeat', true); +// Save an double value to 'decimal' key. +await prefs.setDouble('decimal', 1.5); +// Save an String value to 'action' key. +await prefs.setString('action', 'Start'); +// Save an list of strings to 'items' key. +await prefs.setStringList('items', ['Earth', 'Moon', 'Sun']); +``` + +#### Read data +```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. +final bool? repeat = prefs.getBool('repeat'); +// Try reading data from the 'decimal' key. If it doesn't exist, returns null. +final double? decimal = prefs.getDouble('decimal'); +// Try reading data from the 'action' key. If it doesn't exist, returns null. +final String? action = prefs.getString('action'); +// Try reading data from the 'items' key. If it doesn't exist, returns null. +final List? items = prefs.getStringList('items'); +``` + +#### Remove an entry +```dart +// Remove data for the 'counter' key. +final success = await prefs.remove('counter'); ``` ### Testing @@ -42,5 +62,17 @@ _incrementCounter() async { You can populate `SharedPreferences` with initial values in your tests by running this code: ```dart -SharedPreferences.setMockInitialValues (Map values); +Map values = {'counter': 1}; +SharedPreferences.setMockInitialValues(values); ``` + +### Storage location by platform + +| Platform | Location | +| :--- | :--- | +| Android | SharedPreferences | +| iOS | NSUserDefaults | +| Linux | In the XDG_DATA_HOME directory | +| macOS | NSUserDefaults | +| Web | LocalStorage | +| Windows | In the roaming AppData directory | diff --git a/packages/shared_preferences/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle deleted file mode 100644 index 9284f1c36143..000000000000 --- a/packages/shared_preferences/shared_preferences/android/build.gradle +++ /dev/null @@ -1,61 +0,0 @@ -group 'io.flutter.plugins.sharedpreferences' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -allprojects { - gradle.projectsEvaluated { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" - } - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - baseline file("lint-baseline.xml") - } - dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.9.0' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/shared_preferences/shared_preferences/android/settings.gradle b/packages/shared_preferences/shared_preferences/android/settings.gradle deleted file mode 100644 index 784b49e7ce34..000000000000 --- a/packages/shared_preferences/shared_preferences/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'shared_preferences' 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/android/app/build.gradle b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle index 3b7ee369beee..4cbb7307769c 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle @@ -26,19 +26,19 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.flutter.plugins.example" + applicationId "io.flutter.plugins.sharedpreferencesexample" minSdkVersion 16 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java new file mode 100644 index 000000000000..3d4ea2b1edba --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/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.sharedpreferencesexample; + +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/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java new file mode 100644 index 000000000000..53c757514862 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/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.sharedpreferencesexample; + +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/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 2d5b32857609..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml index 2a12ff8e0009..2fefcb19000e 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,10 @@ + package="io.flutter.plugins.sharedpreferencesexample"> + package="io.flutter.plugins.sharedpreferencesexample"> diff --git a/packages/shared_preferences/shared_preferences/example/android/build.gradle b/packages/shared_preferences/shared_preferences/example/android/build.gradle index 24047dce5d43..21d50697b9e9 100644 --- a/packages/shared_preferences/shared_preferences/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afdda2..b8793d3c0d69 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties @@ -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/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index e8498f473a2c..7244efe99f95 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -2,32 +2,25 @@ // 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'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('$SharedPreferences', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], - }; + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], - }; + const String testString2 = 'goodbye world'; + const bool testBool2 = false; + const int testInt2 = 1337; + const double testDouble2 = 2.71828; + const List testList2 = ['baz', 'quox']; late SharedPreferences preferences; @@ -54,36 +47,36 @@ void main() { testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setString('String', testString2), + preferences.setBool('bool', testBool2), + preferences.setInt('int', testInt2), + preferences.setDouble('double', testDouble2), + preferences.setStringList('List', testList2) ]); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + expect(preferences.getString('String'), testString2); + expect(preferences.getBool('bool'), testBool2); + expect(preferences.getInt('int'), testInt2); + expect(preferences.getDouble('double'), testDouble2); + expect(preferences.getStringList('List'), testList2); }); testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; - await preferences.setString(key, kTestValues['flutter.String']); - await preferences.setBool(key, kTestValues['flutter.bool']); - await preferences.setInt(key, kTestValues['flutter.int']); - await preferences.setDouble(key, kTestValues['flutter.double']); - await preferences.setStringList(key, kTestValues['flutter.List']); + await preferences.setString(key, testString); + await preferences.setBool(key, testBool); + await preferences.setInt(key, testInt); + await preferences.setDouble(key, testDouble); + await preferences.setStringList(key, testList); await preferences.remove(key); expect(preferences.get('testKey'), isNull); }); testWidgets('clearing', (WidgetTester _) async { - await preferences.setString('String', kTestValues['flutter.String']); - await preferences.setBool('bool', kTestValues['flutter.bool']); - await preferences.setInt('int', kTestValues['flutter.int']); - await preferences.setDouble('double', kTestValues['flutter.double']); - await preferences.setStringList('List', kTestValues['flutter.List']); + await preferences.setString('String', testString); + await preferences.setBool('bool', testBool); + await preferences.setInt('int', testInt); + await preferences.setDouble('double', testDouble); + await preferences.setStringList('List', testList); await preferences.clear(); expect(preferences.getString('String'), null); expect(preferences.getBool('bool'), null); @@ -94,49 +87,15 @@ void main() { testWidgets('simultaneous writes', (WidgetTester _) async { final List> writes = >[]; - final int writeCount = 100; + const int writeCount = 100; for (int i = 1; i <= writeCount; i++) { writes.add(preferences.setInt('int', i)); } - List result = await Future.wait(writes, eagerError: true); + final List result = await Future.wait(writes, eagerError: true); // All writes should succeed. - expect(result.where((element) => !element), isEmpty); + expect(result.where((bool element) => !element), isEmpty); // The last write should win. expect(preferences.getInt('int'), writeCount); }); - - testWidgets( - 'string clash with lists, big integers and doubles (Android only)', - (WidgetTester _) async { - await preferences.clear(); - // special prefixes plus a string value - expect( - // prefix for lists - preferences.setString( - 'String', - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + - kTestValues2['flutter.String']), - throwsA(isA())); - await preferences.reload(); - expect(preferences.getString('String'), null); - expect( - // prefix for big integers - preferences.setString( - 'String', - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + - kTestValues2['flutter.String']), - throwsA(isA())); - await preferences.reload(); - expect(preferences.getString('String'), null); - expect( - // prefix for doubles - preferences.setString( - 'String', - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + - kTestValues2['flutter.String']), - throwsA(isA())); - await preferences.reload(); - expect(preferences.getString('String'), null); - }, skip: !Platform.isAndroid); }); } diff --git a/packages/shared_preferences/shared_preferences/example/ios/Podfile b/packages/shared_preferences/shared_preferences/example/ios/Podfile index 3924e59aa0f9..f7d6a5e68c3a 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Podfile +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj index c7567b312596..5040eae278b8 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,26 +9,14 @@ /* Begin PBXBuildFile section */ 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 4E8BDD90E81668641A750C18 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */; }; 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 12E03CD14DABAA3AD3923183 /* 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 */; }; - F76AC2092669B6AE0040C8BC /* SharedPreferencesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - F76AC20B2669B6AE0040C8BC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -63,9 +51,6 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A2FC4F1DC78D7C01312F877F /* 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 = ""; }; D896CE48B6CC2EB7D42CB6B6 /* 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 = ""; }; - F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SharedPreferencesTests.m; sourceTree = ""; }; - F76AC20A2669B6AE0040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,14 +62,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC2032669B6AE0040C8BC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 4E8BDD90E81668641A750C18 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -115,7 +92,6 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - F76AC2072669B6AE0040C8BC /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -126,7 +102,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -164,15 +139,6 @@ name = Frameworks; sourceTree = ""; }; - F76AC2072669B6AE0040C8BC /* RunnerTests */ = { - isa = PBXGroup; - children = ( - F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */, - F76AC20A2669B6AE0040C8BC /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -197,25 +163,6 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - F76AC2052669B6AE0040C8BC /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC20F2669B6AE0040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - DB9B98025BDEFED85B1B62A7 /* [CP] Check Pods Manifest.lock */, - F76AC2022669B6AE0040C8BC /* Sources */, - F76AC2032669B6AE0040C8BC /* Frameworks */, - F76AC2042669B6AE0040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC20C2669B6AE0040C8BC /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -228,11 +175,6 @@ 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; - F76AC2052669B6AE0040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -249,7 +191,6 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - F76AC2052669B6AE0040C8BC /* RunnerTests */, ); }; /* End PBXProject section */ @@ -266,13 +207,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC2042669B6AE0040C8BC /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -322,28 +256,6 @@ 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; }; - DB9B98025BDEFED85B1B62A7 /* [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 */ @@ -357,24 +269,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC2022669B6AE0040C8BC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F76AC2092669B6AE0040C8BC /* SharedPreferencesTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - F76AC20C2669B6AE0040C8BC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC20B2669B6AE0040C8BC /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -543,34 +439,6 @@ }; name = Release; }; - F76AC20D2669B6AE0040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A2FC4F1DC78D7C01312F877F /* 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; - }; - F76AC20E2669B6AE0040C8BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D896CE48B6CC2EB7D42CB6B6 /* 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; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -592,15 +460,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F76AC20F2669B6AE0040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F76AC20D2669B6AE0040C8BC /* Debug */, - F76AC20E2669B6AE0040C8BC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m b/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 103481a2d295..a2e72b446925 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -10,13 +10,15 @@ 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 MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,14 +26,14 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - Future _prefs = SharedPreferences.getInstance(); + final Future _prefs = SharedPreferences.getInstance(); late Future _counter; Future _incrementCounter() async { @@ -39,7 +41,7 @@ class SharedPreferencesDemoState extends State { final int counter = (prefs.getInt('counter') ?? 0) + 1; setState(() { - _counter = prefs.setInt("counter", counter).then((bool success) { + _counter = prefs.setInt('counter', counter).then((bool success) { return counter; }); }); @@ -49,7 +51,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = _prefs.then((SharedPreferences prefs) { - return (prefs.getInt('counter') ?? 0); + return prefs.getInt('counter') ?? 0; }); } @@ -57,7 +59,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 287b6a9d2769..000000000000 --- a/packages/shared_preferences/shared_preferences/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import shared_preferences_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) -} diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 1cb0f185baf4..4ec5cbbb471f 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.8.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/main.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/main.cpp index 126302b0be18..c7dbde1c7123 100644 --- a/packages/shared_preferences/shared_preferences/example/windows/runner/main.cpp +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/main.cpp @@ -11,7 +11,7 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _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()) { diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp index 537728149601..e875ce8b05a9 100644 --- a/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp @@ -13,7 +13,7 @@ void CreateAndAttachConsole() { if (::AllocConsole()) { - FILE *unused; + FILE* unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } diff --git a/packages/shared_preferences/shared_preferences/ios/Assets/.gitkeep b/packages/shared_preferences/shared_preferences/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m b/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m deleted file mode 100644 index 09308d42d762..000000000000 --- a/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m +++ /dev/null @@ -1,83 +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 "FLTSharedPreferencesPlugin.h" - -static NSString *const CHANNEL_NAME = @"plugins.flutter.io/shared_preferences"; - -@implementation FLTSharedPreferencesPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME - binaryMessenger:registrar.messenger]; - [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - NSString *method = [call method]; - NSDictionary *arguments = [call arguments]; - - if ([method isEqualToString:@"getAll"]) { - result(getAllPrefs()); - } else if ([method isEqualToString:@"setBool"]) { - NSString *key = arguments[@"key"]; - NSNumber *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setInt"]) { - NSString *key = arguments[@"key"]; - NSNumber *value = arguments[@"value"]; - // int type in Dart can come to native side in a variety of forms - // It is best to store it as is and send it back when needed. - // Platform channel will handle the conversion. - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setDouble"]) { - NSString *key = arguments[@"key"]; - NSNumber *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setString"]) { - NSString *key = arguments[@"key"]; - NSString *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setStringList"]) { - NSString *key = arguments[@"key"]; - NSArray *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"commit"]) { - // synchronize is deprecated. - // "this method is unnecessary and shouldn't be used." - result(@YES); - } else if ([method isEqualToString:@"remove"]) { - [[NSUserDefaults standardUserDefaults] removeObjectForKey:arguments[@"key"]]; - result(@YES); - } else if ([method isEqualToString:@"clear"]) { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - for (NSString *key in getAllPrefs()) { - [defaults removeObjectForKey:key]; - } - result(@YES); - } else { - result(FlutterMethodNotImplemented); - } - }]; -} - -#pragma mark - Private - -static NSMutableDictionary *getAllPrefs() { - NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; - NSDictionary *prefs = [[NSUserDefaults standardUserDefaults] persistentDomainForName:appDomain]; - NSMutableDictionary *filteredPrefs = [NSMutableDictionary dictionary]; - if (prefs != nil) { - for (NSString *candidateKey in prefs) { - if ([candidateKey hasPrefix:@"flutter."]) { - [filteredPrefs setObject:prefs[candidateKey] forKey:candidateKey]; - } - } - } - return filteredPrefs; -} - -@end diff --git a/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec b/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec deleted file mode 100644 index 0cb5d35e1dd0..000000000000 --- a/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences' - s.version = '0.0.1' - s.summary = 'Flutter Shared Preferences' - s.description = <<-DESC -Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences' } - s.documentation_url = 'https://pub.dev/packages/shared_preferences' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 841d615262de..77f04800a5bb 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -3,14 +3,9 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io' show Platform; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:meta/meta.dart'; -import 'package:shared_preferences_linux/shared_preferences_linux.dart'; -import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; -import 'package:shared_preferences_windows/shared_preferences_windows.dart'; /// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing /// a persistent store for simple data. @@ -21,28 +16,9 @@ class SharedPreferences { static const String _prefix = 'flutter.'; static Completer? _completer; - static bool _manualDartRegistrationNeeded = true; - - static SharedPreferencesStorePlatform get _store { - // TODO(egarciad): Remove once auto registration lands on Flutter stable. - // https://github.com/flutter/flutter/issues/81421. - if (_manualDartRegistrationNeeded) { - // Only do the initial registration if it hasn't already been overridden - // with a non-default instance. - if (!kIsWeb && - SharedPreferencesStorePlatform.instance - is MethodChannelSharedPreferencesStore) { - if (Platform.isLinux) { - SharedPreferencesStorePlatform.instance = SharedPreferencesLinux(); - } else if (Platform.isWindows) { - SharedPreferencesStorePlatform.instance = SharedPreferencesWindows(); - } - } - _manualDartRegistrationNeeded = false; - } - return SharedPreferencesStorePlatform.instance; - } + static SharedPreferencesStorePlatform get _store => + SharedPreferencesStorePlatform.instance; /// Loads and parses the [SharedPreferences] for this app from disk. /// @@ -50,7 +26,8 @@ class SharedPreferences { /// performance-sensitive blocks. static Future getInstance() async { if (_completer == null) { - final completer = Completer(); + final Completer completer = + Completer(); try { final Map preferencesMap = await _getSharedPreferencesMap(); @@ -163,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. @@ -188,7 +165,7 @@ class SharedPreferences { assert(fromSystem != null); // Strip the flutter. prefix from the returned preferences. final Map preferencesMap = {}; - for (String key in fromSystem.keys) { + for (final String key in fromSystem.keys) { assert(key.startsWith(_prefix)); preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; } diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 1e59edf1e12e..a1cea06f5a04 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -1,22 +1,21 @@ name: shared_preferences 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/master/packages/shared_preferences/shared_preferences +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.8 +version: 2.0.15 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.sharedpreferences - pluginClass: SharedPreferencesPlugin + default_package: shared_preferences_android ios: - pluginClass: FLTSharedPreferencesPlugin + default_package: shared_preferences_ios linux: default_package: shared_preferences_linux macos: @@ -29,17 +28,13 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.3.0 - # The design on https://flutter.dev/go/federated-plugins was to leave - # implementation constraints as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. - # TODO(franciscojma): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 - shared_preferences_linux: ^2.0.0 + shared_preferences_android: ^2.0.8 + shared_preferences_ios: ^2.0.8 + shared_preferences_linux: ^2.0.1 shared_preferences_macos: ^2.0.0 shared_preferences_platform_interface: ^2.0.0 shared_preferences_web: ^2.0.0 - shared_preferences_windows: ^2.0.0 + shared_preferences_windows: ^2.0.1 dev_dependencies: flutter_driver: 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 878021a03361..0a02c46404fc 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -11,58 +11,63 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferences', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; + const Map testValues = { + 'flutter.String': testString, + 'flutter.bool': testBool, + 'flutter.int': testInt, + 'flutter.double': testDouble, + 'flutter.List': testList, }; - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], + const String testString2 = 'goodbye world'; + const bool testBool2 = false; + const int testInt2 = 1337; + const double testDouble2 = 2.71828; + const List testList2 = ['baz', 'quox']; + const Map testValues2 = { + 'flutter.String': testString2, + 'flutter.bool': testBool2, + 'flutter.int': testInt2, + 'flutter.double': testDouble2, + 'flutter.List': testList2, }; late FakeSharedPreferencesStore store; late SharedPreferences preferences; setUp(() async { - store = FakeSharedPreferencesStore(kTestValues); + store = FakeSharedPreferencesStore(testValues); SharedPreferencesStorePlatform.instance = store; preferences = await SharedPreferences.getInstance(); store.log.clear(); }); - tearDown(() async { - await preferences.clear(); - await store.clear(); - }); - test('reading', () async { - expect(preferences.get('String'), kTestValues['flutter.String']); - expect(preferences.get('bool'), kTestValues['flutter.bool']); - expect(preferences.get('int'), kTestValues['flutter.int']); - expect(preferences.get('double'), kTestValues['flutter.double']); - expect(preferences.get('List'), kTestValues['flutter.List']); - expect(preferences.getString('String'), kTestValues['flutter.String']); - expect(preferences.getBool('bool'), kTestValues['flutter.bool']); - expect(preferences.getInt('int'), kTestValues['flutter.int']); - expect(preferences.getDouble('double'), kTestValues['flutter.double']); - expect(preferences.getStringList('List'), kTestValues['flutter.List']); + expect(preferences.get('String'), testString); + expect(preferences.get('bool'), testBool); + expect(preferences.get('int'), testInt); + expect(preferences.get('double'), testDouble); + expect(preferences.get('List'), testList); + expect(preferences.getString('String'), testString); + expect(preferences.getBool('bool'), testBool); + expect(preferences.getInt('int'), testInt); + expect(preferences.getDouble('double'), testDouble); + expect(preferences.getStringList('List'), testList); expect(store.log, []); }); test('writing', () async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setString('String', testString2), + preferences.setBool('bool', testBool2), + preferences.setInt('int', testInt2), + preferences.setDouble('double', testDouble2), + preferences.setStringList('List', testList2) ]); expect( store.log, @@ -70,37 +75,37 @@ void main() { isMethodCall('setValue', arguments: [ 'String', 'flutter.String', - kTestValues2['flutter.String'], + testString2, ]), isMethodCall('setValue', arguments: [ 'Bool', 'flutter.bool', - kTestValues2['flutter.bool'], + testBool2, ]), isMethodCall('setValue', arguments: [ 'Int', 'flutter.int', - kTestValues2['flutter.int'], + testInt2, ]), isMethodCall('setValue', arguments: [ 'Double', 'flutter.double', - kTestValues2['flutter.double'], + testDouble2, ]), isMethodCall('setValue', arguments: [ 'StringList', 'flutter.List', - kTestValues2['flutter.List'], + testList2, ]), ], ); store.log.clear(); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + expect(preferences.getString('String'), testString2); + expect(preferences.getBool('bool'), testBool2); + expect(preferences.getInt('int'), testInt2); + expect(preferences.getDouble('double'), testDouble2); + expect(preferences.getStringList('List'), testList2); expect(store.log, equals([])); }); @@ -139,16 +144,15 @@ void main() { }); test('reloading', () async { - await preferences.setString( - 'String', kTestValues['flutter.String'] as String); - expect(preferences.getString('String'), kTestValues['flutter.String']); + await preferences.setString('String', testString); + expect(preferences.getString('String'), testString); SharedPreferences.setMockInitialValues( - kTestValues2.cast()); - expect(preferences.getString('String'), kTestValues['flutter.String']); + testValues2.cast()); + expect(preferences.getString('String'), testString); await preferences.reload(); - expect(preferences.getString('String'), kTestValues2['flutter.String']); + expect(preferences.getString('String'), testString2); }); test('back to back calls should return same instance.', () async { @@ -168,7 +172,7 @@ void main() { group('mocking', () { const String _key = 'dummy'; - const String _prefixedKey = 'flutter.' + _key; + const String _prefixedKey = 'flutter.$_key'; test('test 1', () async { SharedPreferences.setMockInitialValues( @@ -189,13 +193,13 @@ void main() { test('writing copy of strings list', () async { final List myList = []; - await preferences.setStringList("myList", myList); - myList.add("foobar"); + await preferences.setStringList('myList', myList); + myList.add('foobar'); final List cachedList = preferences.getStringList('myList')!; expect(cachedList, []); - cachedList.add("foobar2"); + cachedList.add('foobar2'); expect(preferences.getStringList('myList'), []); }); @@ -223,13 +227,13 @@ class FakeSharedPreferencesStore implements SharedPreferencesStorePlatform { @override Future clear() { - log.add(MethodCall('clear')); + log.add(const MethodCall('clear')); return backend.clear(); } @override Future> getAll() { - log.add(MethodCall('getAll')); + log.add(const MethodCall('getAll')); return backend.getAll(); } diff --git a/packages/device_info/device_info/AUTHORS b/packages/shared_preferences/shared_preferences_android/AUTHORS similarity index 100% rename from packages/device_info/device_info/AUTHORS rename to packages/shared_preferences/shared_preferences_android/AUTHORS diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md new file mode 100644 index 000000000000..51e99ec6d3d5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -0,0 +1,20 @@ +## 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. + +## 2.0.10 + +* Removes dependency on `meta`. + +## 2.0.9 + +* Updates compileSdkVersion to 31. + +## 2.0.8 + +* Split from `shared_preferences` as a federated implementation. diff --git a/packages/shared_preferences/shared_preferences_android/LICENSE b/packages/shared_preferences/shared_preferences_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_android/README.md b/packages/shared_preferences/shared_preferences_android/README.md new file mode 100644 index 000000000000..83d0c5de1151 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/README.md @@ -0,0 +1,11 @@ +# shared\_preferences\_android + +The Android implementation of [`shared_preferences`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_android/android/build.gradle b/packages/shared_preferences/shared_preferences_android/android/build.gradle new file mode 100644 index 000000000000..76106179a8a9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/build.gradle @@ -0,0 +1,61 @@ +group 'io.flutter.plugins.sharedpreferences' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.4.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +allprojects { + gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + dependencies { + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:3.9.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties rename to packages/shared_preferences/shared_preferences_android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/shared_preferences/shared_preferences/android/lint-baseline.xml b/packages/shared_preferences/shared_preferences_android/android/lint-baseline.xml similarity index 100% rename from packages/shared_preferences/shared_preferences/android/lint-baseline.xml rename to packages/shared_preferences/shared_preferences_android/android/lint-baseline.xml diff --git a/packages/shared_preferences/shared_preferences_android/android/settings.gradle b/packages/shared_preferences/shared_preferences_android/android/settings.gradle new file mode 100644 index 000000000000..033d5be261a7 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'shared_preferences_android' diff --git a/packages/shared_preferences/shared_preferences/android/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/shared_preferences/shared_preferences/android/src/main/AndroidManifest.xml rename to packages/shared_preferences/shared_preferences_android/android/src/main/AndroidManifest.xml diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java similarity index 100% rename from packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java rename to packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java similarity index 98% rename from packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java rename to packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java index d41328ee6202..9545fe95c54b 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java +++ b/packages/shared_preferences/shared_preferences_android/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java @@ -11,7 +11,7 @@ /** SharedPreferencesPlugin */ public class SharedPreferencesPlugin implements FlutterPlugin { - private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences"; + private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences_android"; private MethodChannel channel; private MethodCallHandlerImpl handler; diff --git a/packages/shared_preferences/shared_preferences/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java b/packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java similarity index 100% rename from packages/shared_preferences/shared_preferences/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java rename to packages/shared_preferences/shared_preferences_android/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java diff --git a/packages/shared_preferences/shared_preferences_android/example/.gitignore b/packages/shared_preferences/shared_preferences_android/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/shared_preferences/shared_preferences_android/example/.metadata b/packages/shared_preferences/shared_preferences_android/example/.metadata new file mode 100644 index 000000000000..e0e9530fccc9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 79b49b9e1057f90ebf797725233c6b311722de69 + channel: dev + +project_type: app diff --git a/packages/shared_preferences/shared_preferences_android/example/README.md b/packages/shared_preferences/shared_preferences_android/example/README.md new file mode 100644 index 000000000000..c060637c7ec5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/README.md @@ -0,0 +1,3 @@ +# shared_preferences_example + +Demonstrates how to use the shared_preferences plugin. diff --git a/packages/shared_preferences/shared_preferences_android/example/android/.gitignore b/packages/shared_preferences/shared_preferences_android/example/android/.gitignore new file mode 100644 index 000000000000..0a741cb43d66 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle b/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle new file mode 100644 index 000000000000..4cbb7307769c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle @@ -0,0 +1,59 @@ +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 31 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "io.flutter.plugins.sharedpreferencesexample" + minSdkVersion 16 + 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 { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/shared_preferences/shared_preferences_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java similarity index 100% rename from packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/shared_preferences/shared_preferences_android/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java b/packages/shared_preferences/shared_preferences_android/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java new file mode 100644 index 000000000000..304ee4c33326 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/src/androidTest/java/io/flutter/plugins/sharedpreferencesexample/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.sharedpreferences; + +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/shared_preferences/shared_preferences_android/example/android/app/src/debug/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..d60d6f69a862 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..2fefcb19000e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/sensors/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/sensors/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/sensors/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/sensors/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/sensors/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/sensors/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/values-night/styles.xml diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml rename to packages/shared_preferences/shared_preferences_android/example/android/app/src/main/res/values/styles.xml diff --git a/packages/shared_preferences/shared_preferences_android/example/android/app/src/profile/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_android/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..d60d6f69a862 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/shared_preferences/shared_preferences_android/example/android/build.gradle b/packages/shared_preferences/shared_preferences_android/example/android/build.gradle new file mode 100644 index 000000000000..21d50697b9e9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.1' + 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/shared_preferences/shared_preferences_android/example/android/gradle.properties b/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/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/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..b8793d3c0d69 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle b/packages/shared_preferences/shared_preferences_android/example/android/settings.gradle similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle rename to packages/shared_preferences/shared_preferences_android/example/android/settings.gradle 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 new file mode 100644 index 000000000000..275067d98770 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/integration_test/shared_preferences_test.dart @@ -0,0 +1,145 @@ +// Copyright 2013 The Flutter Authors. 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:integration_test/integration_test.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedPreferencesAndroid', () { + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.bool': true, + 'flutter.int': 42, + 'flutter.double': 3.14159, + 'flutter.List': ['foo', 'bar'], + }; + + const Map kTestValues2 = { + 'flutter.String': 'goodbye world', + 'flutter.bool': false, + 'flutter.int': 1337, + 'flutter.double': 2.71828, + 'flutter.List': ['baz', 'quox'], + }; + + late SharedPreferencesStorePlatform preferences; + + setUp(() async { + preferences = SharedPreferencesStorePlatform.instance; + }); + + tearDown(() { + preferences.clear(); + }); + + // Normally the app-facing package adds the prefix, but since this test + // bypasses the app-facing package it needs to be manually added. + String _prefixedKey(String key) { + return 'flutter.$key'; + } + + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], isNull); + expect(values[_prefixedKey('bool')], isNull); + expect(values[_prefixedKey('int')], isNull); + expect(values[_prefixedKey('double')], isNull); + expect(values[_prefixedKey('List')], isNull); + }); + + testWidgets('writing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', _prefixedKey('String'), kTestValues2['flutter.String']!), + preferences.setValue( + 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']!), + preferences.setValue( + 'Int', _prefixedKey('int'), kTestValues2['flutter.int']!), + preferences.setValue( + 'Double', _prefixedKey('double'), kTestValues2['flutter.double']!), + preferences.setValue( + 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']!) + ]); + 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']); + }); + + testWidgets('removing', (WidgetTester _) async { + final String key = _prefixedKey('testKey'); + await preferences.setValue('String', key, kTestValues['flutter.String']!); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); + await preferences.setValue('Int', key, kTestValues['flutter.int']!); + await preferences.setValue('Double', key, kTestValues['flutter.double']!); + await preferences.setValue( + 'StringList', key, kTestValues['flutter.List']!); + await preferences.remove(key); + final Map values = await preferences.getAll(); + expect(values[key], isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await preferences.setValue( + 'String', 'String', kTestValues['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); + await preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']!); + await preferences.setValue( + 'StringList', 'List', kTestValues['flutter.List']!); + await preferences.clear(); + final Map values = await preferences.getAll(); + expect(values['String'], null); + expect(values['bool'], null); + expect(values['int'], null); + expect(values['double'], null); + expect(values['List'], null); + }); + + testWidgets('simultaneous writes', (WidgetTester _) async { + final List> writes = >[]; + const int writeCount = 100; + for (int i = 1; i <= writeCount; 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); + }); + + testWidgets('string clash with lists, big integers and doubles', + (WidgetTester _) async { + final String key = _prefixedKey('akey'); + const String value = 'a string value'; + await preferences.clear(); + + // Special prefixes used to store datatypes that can't be stored directly + // in SharedPreferences as strings instead. + const List specialPrefixes = [ + // Prefix for lists: + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu', + // Prefix for big integers: + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy', + // Prefix for doubles: + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu', + ]; + for (final String prefix in specialPrefixes) { + expect(preferences.setValue('String', key, prefix + value), + throwsA(isA())); + final Map values = await preferences.getAll(); + expect(values[key], null); + } + }); + }); +} diff --git a/packages/shared_preferences/shared_preferences_android/example/lib/main.dart b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart new file mode 100644 index 000000000000..bb513b09f6d5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.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: 'SharedPreferences Demo', + home: SharedPreferencesDemo(), + ); + } +} + +class SharedPreferencesDemo extends StatefulWidget { + const SharedPreferencesDemo({Key? key}) : super(key: key); + + @override + SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); +} + +class SharedPreferencesDemoState extends State { + final SharedPreferencesStorePlatform _prefs = + SharedPreferencesStorePlatform.instance; + late Future _counter; + + // Includes the prefix because this is using the platform interface directly, + // but the prefix (which the native code assumes is present) is added by the + // app-facing package. + static const String _prefKey = 'flutter.counter'; + + Future _incrementCounter() async { + final Map values = await _prefs.getAll(); + final int counter = ((values[_prefKey] as int?) ?? 0) + 1; + + setState(() { + _counter = _prefs.setValue('Int', _prefKey, counter).then((bool success) { + return counter; + }); + }); + } + + @override + void initState() { + super.initState(); + _counter = _prefs.getAll().then((Map values) { + return (values[_prefKey] as int?) ?? 0; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('SharedPreferences Demo'), + ), + body: Center( + child: FutureBuilder( + future: _counter, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const CircularProgressIndicator(); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Text( + 'Button tapped ${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n\n' + 'This should persist across restarts.', + ); + } + } + })), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml new file mode 100644 index 000000000000..d23270ba386f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: shared_preferences_example +description: Demonstrates how to use the shared_preferences plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + shared_preferences_android: + # When depending on this package from a real application you should use: + # shared_preferences_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: ../ + shared_preferences_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_android/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.dart new file mode 100644 index 000000000000..86f447b8959c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/lib/shared_preferences_android.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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +const MethodChannel _kChannel = + MethodChannel('plugins.flutter.io/shared_preferences_android'); + +/// The macOS implementation of [SharedPreferencesStorePlatform]. +/// +/// This class implements the `package:shared_preferences` functionality for Android. +class SharedPreferencesAndroid extends SharedPreferencesStorePlatform { + /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. + static void registerWith() { + SharedPreferencesStorePlatform.instance = SharedPreferencesAndroid(); + } + + @override + Future remove(String key) async { + return (await _kChannel.invokeMethod( + 'remove', + {'key': key}, + ))!; + } + + @override + Future setValue(String valueType, String key, Object value) async { + return (await _kChannel.invokeMethod( + 'set$valueType', + {'key': key, 'value': value}, + ))!; + } + + @override + Future clear() async { + return (await _kChannel.invokeMethod('clear'))!; + } + + @override + Future> getAll() async { + final Map? preferences = + await _kChannel.invokeMapMethod('getAll'); + + if (preferences == null) { + return {}; + } + return preferences; + } +} diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml new file mode 100644 index 000000000000..2d8cc88d3703 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -0,0 +1,27 @@ +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.12 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: shared_preferences + platforms: + android: + package: io.flutter.plugins.sharedpreferences + pluginClass: SharedPreferencesPlugin + dartPluginClass: SharedPreferencesAndroid + +dependencies: + flutter: + sdk: flutter + shared_preferences_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart new file mode 100644 index 000000000000..92d9e8f53be1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart @@ -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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_android/shared_preferences_android.dart'; +import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group(MethodChannelSharedPreferencesStore, () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/shared_preferences_android', + ); + + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + // Create a dummy in-memory implementation to back the mocked method channel + // API to simplify validation of the expected calls. + late InMemorySharedPreferencesStore testData; + + final List log = []; + late SharedPreferencesStorePlatform store; + + setUp(() async { + testData = InMemorySharedPreferencesStore.empty(); + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + if (methodCall.method == 'getAll') { + return await testData.getAll(); + } + if (methodCall.method == 'remove') { + final String key = methodCall.arguments['key']! as String; + return await testData.remove(key); + } + if (methodCall.method == 'clear') { + return await testData.clear(); + } + final RegExp setterRegExp = RegExp(r'set(.*)'); + final Match? match = setterRegExp.matchAsPrefix(methodCall.method); + if (match?.groupCount == 1) { + final String valueType = match!.group(1)!; + final String key = methodCall.arguments['key'] as String; + final Object value = methodCall.arguments['value'] as Object; + return await testData.setValue(valueType, key, value); + } + fail('Unexpected method call: ${methodCall.method}'); + }); + log.clear(); + }); + + test('registered instance', () { + SharedPreferencesAndroid.registerWith(); + expect(SharedPreferencesStorePlatform.instance, + isA()); + }); + + test('getAll', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await store.getAll(), kTestValues); + expect(log.single.method, 'getAll'); + }); + + test('remove', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await store.remove('flutter.String'), true); + expect(await store.remove('flutter.Bool'), true); + expect(await store.remove('flutter.Int'), true); + expect(await store.remove('flutter.Double'), true); + expect(await testData.getAll(), { + 'flutter.StringList': ['foo', 'bar'], + }); + + expect(log, hasLength(4)); + for (final MethodCall call in log) { + expect(call.method, 'remove'); + } + }); + + test('setValue', () async { + store = SharedPreferencesAndroid(); + expect(await testData.getAll(), isEmpty); + for (final String key in kTestValues.keys) { + final Object value = kTestValues[key]!; + expect(await store.setValue(key.split('.').last, key, value), true); + } + expect(await testData.getAll(), kTestValues); + + expect(log, hasLength(5)); + expect(log[0].method, 'setString'); + expect(log[1].method, 'setBool'); + expect(log[2].method, 'setInt'); + expect(log[3].method, 'setDouble'); + expect(log[4].method, 'setStringList'); + }); + + test('clear', () async { + store = SharedPreferencesAndroid(); + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await testData.getAll(), isNotEmpty); + expect(await store.clear(), true); + expect(await testData.getAll(), isEmpty); + expect(log.single.method, 'clear'); + }); + }); +} diff --git a/packages/device_info/device_info_platform_interface/AUTHORS b/packages/shared_preferences/shared_preferences_ios/AUTHORS similarity index 100% rename from packages/device_info/device_info_platform_interface/AUTHORS rename to packages/shared_preferences/shared_preferences_ios/AUTHORS diff --git a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md new file mode 100644 index 000000000000..29ade8d496a1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md @@ -0,0 +1,20 @@ +## 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. + +## 2.0.10 + +* Switches to an in-package method channel implementation. + +## 2.0.9 + +* Removes dependency on `meta`. + +## 2.0.8 + +* Split from `shared_preferences` as a federated implementation. diff --git a/packages/shared_preferences/shared_preferences_ios/LICENSE b/packages/shared_preferences/shared_preferences_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/README.md b/packages/shared_preferences/shared_preferences_ios/README.md new file mode 100644 index 000000000000..5c9ced3b2096 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/README.md @@ -0,0 +1,11 @@ +# shared\_preferences\_ios + +The iOS implementation of [`shared_preferences`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_ios/example/.gitignore b/packages/shared_preferences/shared_preferences_ios/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/.metadata b/packages/shared_preferences/shared_preferences_ios/example/.metadata new file mode 100644 index 000000000000..e0e9530fccc9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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: 79b49b9e1057f90ebf797725233c6b311722de69 + channel: dev + +project_type: app diff --git a/packages/shared_preferences/shared_preferences_ios/example/README.md b/packages/shared_preferences/shared_preferences_ios/example/README.md new file mode 100644 index 000000000000..c060637c7ec5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/README.md @@ -0,0 +1,3 @@ +# shared_preferences_example + +Demonstrates how to use the shared_preferences plugin. 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 new file mode 100644 index 000000000000..3a6bab55eced --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.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 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedPreferencesIos', () { + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.bool': true, + 'flutter.int': 42, + 'flutter.double': 3.14159, + 'flutter.List': ['foo', 'bar'], + }; + + const Map kTestValues2 = { + 'flutter.String': 'goodbye world', + 'flutter.bool': false, + 'flutter.int': 1337, + 'flutter.double': 2.71828, + 'flutter.List': ['baz', 'quox'], + }; + + late SharedPreferencesStorePlatform preferences; + + setUp(() async { + preferences = SharedPreferencesStorePlatform.instance; + }); + + tearDown(() { + preferences.clear(); + }); + + // Normally the app-facing package adds the prefix, but since this test + // bypasses the app-facing package it needs to be manually added. + String _prefixedKey(String key) { + return 'flutter.$key'; + } + + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], isNull); + expect(values[_prefixedKey('bool')], isNull); + expect(values[_prefixedKey('int')], isNull); + expect(values[_prefixedKey('double')], isNull); + expect(values[_prefixedKey('List')], isNull); + }); + + testWidgets('writing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', _prefixedKey('String'), kTestValues2['flutter.String']!), + preferences.setValue( + 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']!), + preferences.setValue( + 'Int', _prefixedKey('int'), kTestValues2['flutter.int']!), + preferences.setValue( + 'Double', _prefixedKey('double'), kTestValues2['flutter.double']!), + preferences.setValue( + 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']!) + ]); + 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']); + }); + + testWidgets('removing', (WidgetTester _) async { + final String key = _prefixedKey('testKey'); + await preferences.setValue('String', key, kTestValues['flutter.String']!); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); + await preferences.setValue('Int', key, kTestValues['flutter.int']!); + await preferences.setValue('Double', key, kTestValues['flutter.double']!); + await preferences.setValue( + 'StringList', key, kTestValues['flutter.List']!); + await preferences.remove(key); + final Map values = await preferences.getAll(); + expect(values[key], isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await preferences.setValue( + 'String', 'String', kTestValues['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); + await preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']!); + await preferences.setValue( + 'StringList', 'List', kTestValues['flutter.List']!); + await preferences.clear(); + final Map values = await preferences.getAll(); + expect(values['String'], null); + expect(values['bool'], null); + expect(values['int'], null); + expect(values['double'], null); + expect(values['List'], null); + }); + }); +} diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/.gitignore b/packages/shared_preferences/shared_preferences_ios/example/ios/.gitignore new file mode 100644 index 000000000000..e96ef602b8d1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.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/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/shared_preferences/shared_preferences_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/ios/Flutter/Debug.xcconfig b/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..d0eccdcaf401 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Release.xcconfig b/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c751c1d022fa --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Podfile b/packages/shared_preferences/shared_preferences_ios/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Podfile @@ -0,0 +1,41 @@ +# 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 + 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/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..c7567b312596 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,607 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4E8BDD90E81668641A750C18 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */; }; + 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 12E03CD14DABAA3AD3923183 /* 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 */; }; + F76AC2092669B6AE0040C8BC /* SharedPreferencesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F76AC20B2669B6AE0040C8BC /* 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 */ + 081A3238A89B77A99B096D83 /* 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 = ""; }; + 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 2D92224A1EC342E7007564B0 /* 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 = ""; }; + 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.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 = ""; }; + 942E815CEF30E101E045B849 /* 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 = ""; }; + 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 = ""; }; + A2FC4F1DC78D7C01312F877F /* 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 = ""; }; + D896CE48B6CC2EB7D42CB6B6 /* 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 = ""; }; + F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SharedPreferencesTests.m; sourceTree = ""; }; + F76AC20A2669B6AE0040C8BC /* 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 = ( + 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC2032669B6AE0040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E8BDD90E81668641A750C18 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */, + 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */, + A2FC4F1DC78D7C01312F877F /* Pods-RunnerTests.debug.xcconfig */, + D896CE48B6CC2EB7D42CB6B6 /* 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 = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F76AC2072669B6AE0040C8BC /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */, + 2D92224A1EC342E7007564B0 /* 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 = ( + 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */, + 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F76AC2072669B6AE0040C8BC /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */, + F76AC20A2669B6AE0040C8BC /* Info.plist */, + ); + path = RunnerTests; + 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 */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F76AC2052669B6AE0040C8BC /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC20F2669B6AE0040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + DB9B98025BDEFED85B1B62A7 /* [CP] Check Pods Manifest.lock */, + F76AC2022669B6AE0040C8BC /* Sources */, + F76AC2032669B6AE0040C8BC /* Frameworks */, + F76AC2042669B6AE0040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC20C2669B6AE0040C8BC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F76AC2052669B6AE0040C8BC = { + 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 */, + F76AC2052669B6AE0040C8BC /* 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; + }; + F76AC2042669B6AE0040C8BC /* 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"; + }; + 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; + }; + DB9B98025BDEFED85B1B62A7 /* [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 */, + 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76AC2022669B6AE0040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC2092669B6AE0040C8BC /* SharedPreferencesTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F76AC20C2669B6AE0040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC20B2669B6AE0040C8BC /* 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.sharedPreferencesExample; + 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.sharedPreferencesExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F76AC20D2669B6AE0040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A2FC4F1DC78D7C01312F877F /* 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; + }; + F76AC20E2669B6AE0040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D896CE48B6CC2EB7D42CB6B6 /* 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; + }; +/* 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; + }; + F76AC20F2669B6AE0040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC20D2669B6AE0040C8BC /* Debug */, + F76AC20E2669B6AE0040C8BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..5e29b432c48c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/share/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/share/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.h diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.m b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..b790a0a52635 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..caf998393333 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/share/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/share/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/share/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..22fc4c23715d --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + shared_preferences_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Runner-Bridging-Header.h b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..eb7e8ba8052f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_ios/example/ios/Runner/main.m b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/video_player/video_player/example/ios/RunnerTests/Info.plist b/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/video_player/video_player/example/ios/RunnerTests/Info.plist rename to packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/shared_preferences/shared_preferences/example/ios/RunnerTests/SharedPreferencesTests.m b/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.m similarity index 78% rename from packages/shared_preferences/shared_preferences/example/ios/RunnerTests/SharedPreferencesTests.m rename to packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.m index 08116fc38ee7..792f6c111b82 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/RunnerTests/SharedPreferencesTests.m +++ b/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.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 shared_preferences; +@import shared_preferences_ios; @import XCTest; @interface SharedPreferencesTests : XCTestCase @@ -11,7 +11,7 @@ @interface SharedPreferencesTests : XCTestCase @implementation SharedPreferencesTests - (void)testPlugin { - FLTSharedPreferencesPlugin* plugin = [[FLTSharedPreferencesPlugin alloc] init]; + FLTSharedPreferencesPlugin *plugin = [[FLTSharedPreferencesPlugin alloc] init]; XCTAssertNotNil(plugin); } diff --git a/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart b/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart new file mode 100644 index 000000000000..bb513b09f6d5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.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: 'SharedPreferences Demo', + home: SharedPreferencesDemo(), + ); + } +} + +class SharedPreferencesDemo extends StatefulWidget { + const SharedPreferencesDemo({Key? key}) : super(key: key); + + @override + SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); +} + +class SharedPreferencesDemoState extends State { + final SharedPreferencesStorePlatform _prefs = + SharedPreferencesStorePlatform.instance; + late Future _counter; + + // Includes the prefix because this is using the platform interface directly, + // but the prefix (which the native code assumes is present) is added by the + // app-facing package. + static const String _prefKey = 'flutter.counter'; + + Future _incrementCounter() async { + final Map values = await _prefs.getAll(); + final int counter = ((values[_prefKey] as int?) ?? 0) + 1; + + setState(() { + _counter = _prefs.setValue('Int', _prefKey, counter).then((bool success) { + return counter; + }); + }); + } + + @override + void initState() { + super.initState(); + _counter = _prefs.getAll().then((Map values) { + return (values[_prefKey] as int?) ?? 0; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('SharedPreferences Demo'), + ), + body: Center( + child: FutureBuilder( + future: _counter, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const CircularProgressIndicator(); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Text( + 'Button tapped ${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n\n' + 'This should persist across restarts.', + ); + } + } + })), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml new file mode 100644 index 000000000000..9f5f7124669d --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: shared_preferences_example +description: Demonstrates how to use the shared_preferences plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + shared_preferences_ios: + # When depending on this package from a real application you should use: + # shared_preferences_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: ../ + shared_preferences_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_ios/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep b/packages/shared_preferences/shared_preferences_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep rename to packages/shared_preferences/shared_preferences_ios/ios/Assets/.gitkeep diff --git a/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.h b/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.h similarity index 100% rename from packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.h rename to packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.h diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m b/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m new file mode 100644 index 000000000000..bb11da2b5406 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.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 "FLTSharedPreferencesPlugin.h" +#import "messages.g.h" + +static NSMutableDictionary *getAllPrefs() { + NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; + NSDictionary *prefs = [[NSUserDefaults standardUserDefaults] persistentDomainForName:appDomain]; + NSMutableDictionary *filteredPrefs = [NSMutableDictionary dictionary]; + if (prefs != nil) { + for (NSString *candidateKey in prefs) { + if ([candidateKey hasPrefix:@"flutter."]) { + [filteredPrefs setObject:prefs[candidateKey] forKey:candidateKey]; + } + } + } + return filteredPrefs; +} + +@interface FLTSharedPreferencesPlugin () +@end + +@implementation FLTSharedPreferencesPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTSharedPreferencesPlugin *plugin = [[FLTSharedPreferencesPlugin alloc] init]; + UserDefaultsApiSetup(registrar.messenger, plugin); +} + +// Must not return nil unless "error" is set. +- (nullable NSDictionary *)getAllWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return getAllPrefs(); +} + +- (void)clearWithError:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + for (NSString *key in getAllPrefs()) { + [defaults removeObjectForKey:key]; + } +} + +- (void)removeKey:(nonnull NSString *)key + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; +} + +- (void)setBoolKey:(nonnull NSString *)key + value:(nonnull NSNumber *)value + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key]; +} + +- (void)setDoubleKey:(nonnull NSString *)key + value:(nonnull NSNumber *)value + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key]; +} + +- (void)setValueKey:(nonnull NSString *)key + value:(nonnull NSString *)value + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { + [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; +} + +@end diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h new file mode 100644 index 000000000000..592402344a04 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h @@ -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. +// Autogenerated from Pigeon (v1.0.16), 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 UserDefaultsApi. +NSObject *UserDefaultsApiGetCodec(void); + +@protocol UserDefaultsApi +- (void)removeKey:(NSString *)key error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setBoolKey:(NSString *)key + value:(NSNumber *)value + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setDoubleKey:(NSString *)key + value:(NSNumber *)value + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setValueKey:(NSString *)key value:(id)value error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSDictionary *)getAllWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)clearWithError:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void UserDefaultsApiSetup(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m new file mode 100644 index 000000000000..ea8c45c7a66c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m @@ -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. +// Autogenerated from Pigeon (v1.0.16), 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, + }; +} + +@interface UserDefaultsApiCodecReader : FlutterStandardReader +@end +@implementation UserDefaultsApiCodecReader +@end + +@interface UserDefaultsApiCodecWriter : FlutterStandardWriter +@end +@implementation UserDefaultsApiCodecWriter +@end + +@interface UserDefaultsApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation UserDefaultsApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[UserDefaultsApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[UserDefaultsApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *UserDefaultsApiGetCodec() { + static dispatch_once_t s_pred = 0; + static FlutterStandardMessageCodec *s_sharedObject = nil; + dispatch_once(&s_pred, ^{ + UserDefaultsApiCodecReaderWriter *readerWriter = + [[UserDefaultsApiCodecReaderWriter alloc] init]; + s_sharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return s_sharedObject; +} + +void UserDefaultsApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.remove" + binaryMessenger:binaryMessenger + codec:UserDefaultsApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(removeKey:error:)], + @"UserDefaultsApi api (%@) doesn't respond to @selector(removeKey:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_key = args[0]; + FlutterError *error; + [api removeKey:arg_key error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setBool" + binaryMessenger:binaryMessenger + codec:UserDefaultsApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setBoolKey:value:error:)], + @"UserDefaultsApi api (%@) doesn't respond to @selector(setBoolKey:value:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_key = args[0]; + NSNumber *arg_value = args[1]; + FlutterError *error; + [api setBoolKey:arg_key value:arg_value error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setDouble" + binaryMessenger:binaryMessenger + codec:UserDefaultsApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setDoubleKey:value:error:)], + @"UserDefaultsApi api (%@) doesn't respond to @selector(setDoubleKey:value:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_key = args[0]; + NSNumber *arg_value = args[1]; + FlutterError *error; + [api setDoubleKey:arg_key value:arg_value error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setValue" + binaryMessenger:binaryMessenger + codec:UserDefaultsApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setValueKey:value:error:)], + @"UserDefaultsApi api (%@) doesn't respond to @selector(setValueKey:value:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSString *arg_key = args[0]; + id arg_value = args[1]; + FlutterError *error; + [api setValueKey:arg_key value:arg_value error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.getAll" + binaryMessenger:binaryMessenger + codec:UserDefaultsApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getAllWithError:)], + @"UserDefaultsApi api (%@) doesn't respond to @selector(getAllWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSDictionary *output = [api getAllWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.clear" + binaryMessenger:binaryMessenger + codec:UserDefaultsApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(clearWithError:)], + @"UserDefaultsApi api (%@) doesn't respond to @selector(clearWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api clearWithError:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec b/packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec new file mode 100644 index 000000000000..4126ed0a8cc2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'shared_preferences_ios' + s.version = '0.0.1' + s.summary = 'Flutter Shared Preferences' + s.description = <<-DESC +Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_ios' } + s.documentation_url = 'https://pub.dev/packages/shared_preferences' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end + diff --git a/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart b/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart new file mode 100644 index 000000000000..0e76291fb655 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart @@ -0,0 +1,179 @@ +// Copyright 2013 The Flutter 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. +// 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 _UserDefaultsApiCodec extends StandardMessageCodec { + const _UserDefaultsApiCodec(); +} + +class UserDefaultsApi { + /// Constructor for [UserDefaultsApi]. 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. + UserDefaultsApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _UserDefaultsApiCodec(); + + Future remove(String arg_key) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.remove', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_key]) 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; + } + } + + Future setBool(String arg_key, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setBool', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_key, 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setDouble(String arg_key, double arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setDouble', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_key, 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setValue(String arg_key, Object arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setValue', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_key, 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future> getAll() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.getAll', 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as Map?)! + .cast(); + } + } + + Future clear() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.clear', 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart b/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart new file mode 100644 index 000000000000..10638840804e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'messages.g.dart'; + +typedef _Setter = Future Function(String key, Object value); + +/// iOS implementation of shared_preferences. +class SharedPreferencesIOS extends SharedPreferencesStorePlatform { + final UserDefaultsApi _api = UserDefaultsApi(); + late final Map _setters = { + 'Bool': (String key, Object value) { + return _api.setBool(key, value as bool); + }, + 'Double': (String key, Object value) { + return _api.setDouble(key, value as double); + }, + 'Int': (String key, Object value) { + return _api.setValue(key, value as int); + }, + 'String': (String key, Object value) { + return _api.setValue(key, value as String); + }, + 'StringList': (String key, Object value) { + return _api.setValue(key, value as List); + }, + }; + + /// Registers this class as the default instance of [PathProviderPlatform]. + static void registerWith() { + SharedPreferencesStorePlatform.instance = SharedPreferencesIOS(); + } + + @override + Future clear() async { + await _api.clear(); + return true; + } + + @override + Future> getAll() async { + final Map result = await _api.getAll(); + return result.cast(); + } + + @override + Future remove(String key) async { + await _api.remove(key); + return true; + } + + @override + Future setValue(String valueType, String key, Object value) async { + final _Setter? setter = _setters[valueType]; + if (setter == null) { + throw PlatformException( + code: 'InvalidOperation', + message: '"$valueType" is not a supported type.'); + } + await setter(key, value); + return true; + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt b/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt new file mode 100644 index 000000000000..fb682b1ab965 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.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/shared_preferences/shared_preferences_ios/pigeons/messages.dart b/packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart new file mode 100644 index 000000000000..6b5648f9e2f0 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/pigeons/messages.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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/messages.g.dart', + dartTestOut: 'test/messages.g.dart', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + copyrightHeader: 'pigeons/copyright_header.txt', +)) +@HostApi(dartHostTestHandler: 'TestUserDefaultsApi') +abstract class UserDefaultsApi { + void remove(String key); + void setBool(String key, bool value); + void setDouble(String key, double value); + void setValue(String key, Object value); + Map getAll(); + void clear(); +} diff --git a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml new file mode 100644 index 000000000000..a8bde2e9f87f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml @@ -0,0 +1,27 @@ +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.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: shared_preferences + platforms: + ios: + dartPluginClass: SharedPreferencesIOS + pluginClass: FLTSharedPreferencesPlugin + +dependencies: + flutter: + sdk: flutter + shared_preferences_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pigeon: ^1.0.16 diff --git a/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart b/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart new file mode 100644 index 000000000000..12fbc0635784 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart @@ -0,0 +1,145 @@ +// Copyright 2013 The Flutter 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. +// 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/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:shared_preferences_ios/messages.g.dart'; + +class _TestUserDefaultsApiCodec extends StandardMessageCodec { + const _TestUserDefaultsApiCodec(); +} + +abstract class TestUserDefaultsApi { + static const MessageCodec codec = _TestUserDefaultsApiCodec(); + + void remove(String key); + void setBool(String key, bool value); + void setDouble(String key, double value); + void setValue(String key, Object value); + Map getAll(); + void clear(); + static void setup(TestUserDefaultsApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.remove', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.remove was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.remove was null, expected non-null String.'); + api.remove(arg_key!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setBool', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null, expected non-null String.'); + final bool? arg_value = (args[1] as bool?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null, expected non-null bool.'); + api.setBool(arg_key!, arg_value!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setDouble', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null, expected non-null String.'); + final double? arg_value = (args[1] as double?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null, expected non-null double.'); + api.setDouble(arg_key!, arg_value!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setValue', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null, expected non-null String.'); + final Object? arg_value = (args[1] as Object?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null, expected non-null Object.'); + api.setValue(arg_key!, arg_value!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.getAll', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final Map output = api.getAll(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.clear', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.clear(); + return {}; + }); + } + } + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart b/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart new file mode 100644 index 000000000000..efafb230d9de --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_ios/shared_preferences_ios.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +import 'messages.g.dart'; + +class _MockSharedPreferencesApi implements TestUserDefaultsApi { + final Map items = {}; + + @override + Map getAll() { + return items; + } + + @override + void remove(String key) { + items.remove(key); + } + + @override + void setBool(String key, bool value) { + items[key] = value; + } + + @override + void setDouble(String key, double value) { + items[key] = value; + } + + @override + void setValue(String key, Object value) { + items[key] = value; + } + + @override + void clear() { + items.clear(); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + _MockSharedPreferencesApi api = _MockSharedPreferencesApi(); + SharedPreferencesIOS plugin = SharedPreferencesIOS(); + + setUp(() { + api = _MockSharedPreferencesApi(); + TestUserDefaultsApi.setup(api); + plugin = SharedPreferencesIOS(); + }); + + test('registerWith', () { + SharedPreferencesIOS.registerWith(); + expect( + SharedPreferencesStorePlatform.instance, isA()); + }); + + test('remove', () async { + api.items['flutter.hi'] = 'world'; + expect(await plugin.remove('flutter.hi'), isTrue); + expect(api.items.containsKey('flutter.hi'), isFalse); + }); + + test('clear', () async { + api.items['flutter.hi'] = 'world'; + expect(await plugin.clear(), isTrue); + expect(api.items.containsKey('flutter.hi'), isFalse); + }); + + test('getAll', () async { + api.items['flutter.hi'] = 'world'; + api.items['flutter.bye'] = 'dust'; + final Map all = await plugin.getAll(); + expect(all.length, 2); + expect(all['flutter.hi'], api.items['flutter.hi']); + expect(all['flutter.bye'], api.items['flutter.bye']); + }); + + test('setValue', () async { + expect(await plugin.setValue('Bool', 'flutter.Bool', true), isTrue); + expect(api.items['flutter.Bool'], true); + expect(await plugin.setValue('Double', 'flutter.Double', 1.5), isTrue); + expect(api.items['flutter.Double'], 1.5); + expect(await plugin.setValue('Int', 'flutter.Int', 12), isTrue); + expect(api.items['flutter.Int'], 12); + expect(await plugin.setValue('String', 'flutter.String', 'hi'), isTrue); + expect(api.items['flutter.String'], 'hi'); + expect( + await plugin + .setValue('StringList', 'flutter.StringList', ['hi']), + isTrue); + expect(api.items['flutter.StringList'], ['hi']); + }); + + test('setValue with unsupported type', () { + expect(() async { + await plugin.setValue('Map', 'flutter.key', {}); + }, throwsA(isA())); + }); +} diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index fc09bec23591..f0cb8322f385 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,22 @@ +## 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`. + +## 2.0.4 + +* Removes dependency on `meta`. + +## 2.0.3 + +* Removed obsolete `pluginClass: none` from pubpsec. +* Fixes newly enabled analyzer options. + ## 2.0.2 * Updated installation instructions in README. diff --git a/packages/shared_preferences/shared_preferences_linux/example/README.md b/packages/shared_preferences/shared_preferences_linux/example/README.md index 7dd9e9c4aa42..c060637c7ec5 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/README.md +++ b/packages/shared_preferences/shared_preferences_linux/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_linux/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart index 5dba3def31d0..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'; @@ -12,7 +10,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesLinux', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -20,7 +18,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -31,7 +29,7 @@ void main() { late SharedPreferencesLinux preferences; setUp(() async { - preferences = SharedPreferencesLinux.instance; + preferences = SharedPreferencesLinux(); }); tearDown(() { @@ -39,7 +37,7 @@ void main() { }); testWidgets('reading', (WidgetTester _) async { - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['String'], isNull); expect(all['bool'], isNull); expect(all['int'], isNull); @@ -50,14 +48,15 @@ void main() { testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( - 'String', 'String', kTestValues2['flutter.String']), - preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), - preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + 'String', 'String', kTestValues2['flutter.String']!), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']!), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']!), + preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']!), preferences.setValue( - 'Double', 'double', kTestValues2['flutter.double']), - preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) + 'StringList', 'List', kTestValues2['flutter.List']!) ]); - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['String'], kTestValues2['flutter.String']); expect(all['bool'], kTestValues2['flutter.bool']); expect(all['int'], kTestValues2['flutter.int']); @@ -68,28 +67,30 @@ void main() { testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; - await Future.wait([ - preferences.setValue('String', key, kTestValues['flutter.String']), - preferences.setValue('Bool', key, kTestValues['flutter.bool']), - preferences.setValue('Int', key, kTestValues['flutter.int']), - preferences.setValue('Double', key, kTestValues['flutter.double']), - preferences.setValue('StringList', key, kTestValues['flutter.List']) + await Future.wait(>[ + preferences.setValue('String', key, kTestValues['flutter.String']!), + preferences.setValue('Bool', key, kTestValues['flutter.bool']!), + preferences.setValue('Int', key, kTestValues['flutter.int']!), + preferences.setValue('Double', key, kTestValues['flutter.double']!), + preferences.setValue('StringList', key, kTestValues['flutter.List']!) ]); await preferences.remove(key); - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['testKey'], isNull); }); testWidgets('clearing', (WidgetTester _) async { await Future.wait(>[ - preferences.setValue('String', 'String', kTestValues['flutter.String']), - preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']), - preferences.setValue('Int', 'int', kTestValues['flutter.int']), - preferences.setValue('Double', 'double', kTestValues['flutter.double']), - preferences.setValue('StringList', 'List', kTestValues['flutter.List']) + preferences.setValue( + 'String', 'String', kTestValues['flutter.String']!), + preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!), + preferences.setValue('Int', 'int', kTestValues['flutter.int']!), + preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']!), + preferences.setValue('StringList', 'List', kTestValues['flutter.List']!) ]); await preferences.clear(); - final all = await preferences.getAll(); + final Map all = await preferences.getAll(); expect(all['String'], null); expect(all['bool'], null); expect(all['int'], null); diff --git a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart index aee71d00d44d..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,13 +10,15 @@ 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 MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,18 +26,18 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - final prefs = SharedPreferencesLinux.instance; + final SharedPreferencesLinux prefs = SharedPreferencesLinux(); late Future _counter; Future _incrementCounter() async { - final values = await prefs.getAll(); + final Map values = await prefs.getAll(); final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { @@ -49,7 +51,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] as int? ?? 0); + return values['counter'] as int? ?? 0; }); } @@ -57,7 +59,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23d05..000000000000 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index d34973b9dde6..4d44d4e69f93 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.8.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart index 5ec988216074..b6a9a5bca4ff 100644 --- a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart +++ b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart @@ -7,7 +7,7 @@ import 'dart:convert' show json; import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:path/path.dart' as path; import 'package:path_provider_linux/path_provider_linux.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; @@ -16,14 +16,14 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor /// /// This class implements the `package:shared_preferences` functionality for Linux. class SharedPreferencesLinux extends SharedPreferencesStorePlatform { - /// The default instance of [SharedPreferencesLinux] to use. - /// TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. - /// https://github.com/flutter/flutter/issues/81421 + /// Deprecated instance of [SharedPreferencesLinux]. + /// Use [SharedPreferencesStorePlatform.instance] instead. + @Deprecated('Use `SharedPreferencesStorePlatform.instance` instead.') static SharedPreferencesLinux instance = SharedPreferencesLinux(); /// Registers the Linux implementation. static void registerWith() { - SharedPreferencesStorePlatform.instance = instance; + SharedPreferencesStorePlatform.instance = SharedPreferencesLinux(); } /// Local copy of preferences @@ -31,13 +31,18 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// File system used to store to disk. Exposed for testing only. @visibleForTesting - FileSystem fs = LocalFileSystem(); + FileSystem fs = const LocalFileSystem(); + + /// The path_provider_linux instance used to find the support directory. + @visibleForTesting + PathProviderLinux pathProvider = PathProviderLinux(); /// Gets the file where the preferences are stored. Future _getLocalDataFile() async { - final pathProvider = PathProviderLinux(); - final directory = await pathProvider.getApplicationSupportPath(); - if (directory == null) return null; + final String? directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) { + return null; + } return fs.file(path.join(directory, 'shared_preferences.json')); } @@ -48,12 +53,15 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { return _cachedPreferences!; } - Map preferences = {}; + Map preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile != null && localDataFile.existsSync()) { - String stringMap = localDataFile.readAsStringSync(); + final String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { - preferences = json.decode(stringMap).cast(); + final Object? data = json.decode(stringMap); + if (data is Map) { + preferences = data.cast(); + } } } _cachedPreferences = preferences; @@ -64,18 +72,18 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// succeeded. Future _writePreferences(Map preferences) async { try { - var localDataFile = await _getLocalDataFile(); + final File? localDataFile = await _getLocalDataFile(); if (localDataFile == null) { - print("Unable to determine where to write preferences."); + print('Unable to determine where to write preferences.'); return false; } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } - var stringMap = json.encode(preferences); + final String stringMap = json.encode(preferences); localDataFile.writeAsStringSync(stringMap); } catch (e) { - print("Error saving preferences to disk: $e"); + print('Error saving preferences to disk: $e'); return false; } return true; @@ -83,7 +91,7 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { @override Future clear() async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.clear(); return _writePreferences(preferences); } @@ -95,14 +103,14 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.remove(key); return _writePreferences(preferences); } @override Future setValue(String valueType, String key, Object value) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences[key] = value; return _writePreferences(preferences); } diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index c03e49e042e2..922437256748 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -1,12 +1,12 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin -repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux +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.0.2 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,18 +14,16 @@ flutter: platforms: linux: dartPluginClass: SharedPreferencesLinux - pluginClass: none dependencies: file: ^6.0.0 - meta: ^1.3.0 flutter: sdk: flutter path: ^1.8.0 path_provider_linux: ^2.0.0 + path_provider_platform_interface: ^2.0.0 shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart index 62ec2b66c07a..57acd17f5094 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 @@ -5,25 +5,27 @@ import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:shared_preferences_linux/shared_preferences_linux.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { late MemoryFileSystem fs; + late PathProviderLinux pathProvider; SharedPreferencesLinux.registerWith(); setUp(() { fs = MemoryFileSystem.test(); + pathProvider = FakePathProviderLinux(); }); Future _getFilePath() async { - final pathProvider = PathProviderLinux(); - final directory = await pathProvider.getApplicationSupportPath(); + final String? directory = await pathProvider.getApplicationSupportPath(); return path.join(directory!, 'shared_preferences.json'); } - _writeTestFile(String value) async { + Future _writeTestFile(String value) async { fs.file(await _getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); @@ -34,21 +36,23 @@ void main() { } SharedPreferencesLinux _getPreferences() { - var prefs = SharedPreferencesLinux(); + final SharedPreferencesLinux prefs = SharedPreferencesLinux(); prefs.fs = fs; + prefs.pathProvider = pathProvider; return prefs; } test('registered instance', () { + SharedPreferencesLinux.registerWith(); expect( SharedPreferencesStorePlatform.instance, isA()); }); test('getAll', () async { await _writeTestFile('{"key1": "one", "key2": 2}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); - var values = await prefs.getAll(); + final Map values = await prefs.getAll(); expect(values, hasLength(2)); expect(values['key1'], 'one'); expect(values['key2'], 2); @@ -56,7 +60,7 @@ void main() { test('remove', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); await prefs.remove('key2'); @@ -65,7 +69,7 @@ void main() { test('setValue', () async { await _writeTestFile('{}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); await prefs.setValue('', 'key1', 'one'); await prefs.setValue('', 'key2', 2); @@ -75,9 +79,32 @@ void main() { test('clear', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesLinux prefs = _getPreferences(); await prefs.clear(); expect(await _readTestFile(), '{}'); }); } + +/// Fake implementation of PathProviderLinux that returns hard-coded paths, +/// allowing tests to run on any platform. +/// +/// Note that this should only be used with an in-memory filesystem, as the +/// path it returns is a root path that does not actually exist on Linux. +class FakePathProviderLinux extends PathProviderPlatform + implements PathProviderLinux { + @override + Future getApplicationSupportPath() async => r'/appsupport'; + + @override + Future getTemporaryPath() async => null; + + @override + Future getLibraryPath() async => null; + + @override + Future getApplicationDocumentsPath() async => null; + + @override + Future getDownloadsPath() async => null; +} diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index 2f7e0edf9a51..8ba116a74fe0 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,14 @@ +## 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. +* Fixes newly enabled analyzer options. + ## 2.0.2 * Add native unit tests. diff --git a/packages/shared_preferences/shared_preferences_macos/example/README.md b/packages/shared_preferences/shared_preferences_macos/example/README.md index 7dd9e9c4aa42..c060637c7ec5 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/README.md +++ b/packages/shared_preferences/shared_preferences_macos/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_macos/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart index e7a267f7e51a..874ceb4c51a7 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,16 +2,15 @@ // 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:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesMacOS', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -19,7 +18,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -55,15 +54,15 @@ void main() { 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']); @@ -75,12 +74,12 @@ void main() { testWidgets('removing', (WidgetTester _) async { final String key = _prefixedKey('testKey'); - await preferences.setValue('String', key, kTestValues['flutter.String']); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']); - await preferences.setValue('Int', key, kTestValues['flutter.int']); - await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue('String', key, kTestValues['flutter.String']!); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); + await preferences.setValue('Int', key, kTestValues['flutter.int']!); + await preferences.setValue('Double', key, kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']); + 'StringList', key, kTestValues['flutter.List']!); await preferences.remove(key); final Map values = await preferences.getAll(); expect(values[key], isNull); @@ -88,13 +87,13 @@ void main() { testWidgets('clearing', (WidgetTester _) async { await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + 'String', 'String', kTestValues['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']); + 'Double', 'double', kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']); + 'StringList', 'List', kTestValues['flutter.List']!); await preferences.clear(); final Map values = await preferences.getAll(); expect(values['String'], null); diff --git a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart index fb85c301f623..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,13 +10,15 @@ 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 MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,14 +26,14 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - SharedPreferencesStorePlatform _prefs = + final SharedPreferencesStorePlatform _prefs = SharedPreferencesStorePlatform.instance; late Future _counter; @@ -63,7 +65,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 287b6a9d2769..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import shared_preferences_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml index e6bf972cf5b4..b9dfb75c92e7 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml @@ -4,12 +4,11 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.8" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - shared_preferences_platform_interface: ^2.0.0 shared_preferences_macos: # When depending on this package from a real application you should use: # shared_preferences_macos: ^x.y.z @@ -17,6 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_driver: diff --git a/packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.dart b/packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.dart new file mode 100644 index 000000000000..a97fe131af5c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +const MethodChannel _kChannel = + MethodChannel('plugins.flutter.io/shared_preferences_macos'); + +/// The macOS implementation of [SharedPreferencesStorePlatform]. +/// +/// This class implements the `package:shared_preferences` functionality for macOS. +class SharedPreferencesMacOS extends SharedPreferencesStorePlatform { + /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. + static void registerWith() { + SharedPreferencesStorePlatform.instance = SharedPreferencesMacOS(); + } + + @override + Future remove(String key) async { + return (await _kChannel.invokeMethod( + 'remove', + {'key': key}, + ))!; + } + + @override + Future setValue(String valueType, String key, Object value) async { + return (await _kChannel.invokeMethod( + 'set$valueType', + {'key': key, 'value': value}, + ))!; + } + + @override + Future clear() async { + return (await _kChannel.invokeMethod('clear'))!; + } + + @override + Future> getAll() async { + final Map? preferences = + await _kChannel.invokeMapMethod('getAll'); + + if (preferences == null) { + return {}; + } + return preferences; + } +} diff --git a/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift index 2cf345cf0b04..91b42441adda 100644 --- a/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift +++ b/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift @@ -8,7 +8,7 @@ import Foundation public class SharedPreferencesPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( - name: "plugins.flutter.io/shared_preferences", + name: "plugins.flutter.io/shared_preferences_macos", binaryMessenger: registrar.messenger) let instance = SharedPreferencesPlugin() registrar.addMethodCallDelegate(instance, channel: channel) 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 9f364c5b6bc1..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 @@ -8,10 +8,10 @@ Pod::Spec.new do |s| s.description = <<-DESC Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos' + 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 6e351e86fb1a..9259ef5888fa 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml @@ -1,12 +1,12 @@ name: shared_preferences_macos description: macOS implementation of the shared_preferences plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos +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.2 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,6 +14,7 @@ flutter: platforms: macos: pluginClass: SharedPreferencesPlugin + dartPluginClass: SharedPreferencesMacOS dependencies: flutter: @@ -21,4 +22,5 @@ dependencies: shared_preferences_platform_interface: ^2.0.0 dev_dependencies: - pedantic: ^1.10.0 + flutter_test: + sdk: flutter diff --git a/packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart b/packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart new file mode 100644 index 000000000000..cd858f48179d --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart @@ -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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_macos/shared_preferences_macos.dart'; +import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group(MethodChannelSharedPreferencesStore, () { + const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/shared_preferences_macos', + ); + + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.Bool': true, + 'flutter.Int': 42, + 'flutter.Double': 3.14159, + 'flutter.StringList': ['foo', 'bar'], + }; + // Create a dummy in-memory implementation to back the mocked method channel + // API to simplify validation of the expected calls. + late InMemorySharedPreferencesStore testData; + + final List log = []; + late SharedPreferencesStorePlatform store; + + setUp(() async { + testData = InMemorySharedPreferencesStore.empty(); + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + if (methodCall.method == 'getAll') { + return await testData.getAll(); + } + if (methodCall.method == 'remove') { + final String key = (methodCall.arguments['key'] as String?)!; + return await testData.remove(key); + } + if (methodCall.method == 'clear') { + return await testData.clear(); + } + final RegExp setterRegExp = RegExp(r'set(.*)'); + final Match? match = setterRegExp.matchAsPrefix(methodCall.method); + if (match?.groupCount == 1) { + final String valueType = match!.group(1)!; + final String key = (methodCall.arguments['key'] as String?)!; + final Object value = (methodCall.arguments['value'] as Object?)!; + return await testData.setValue(valueType, key, value); + } + fail('Unexpected method call: ${methodCall.method}'); + }); + log.clear(); + }); + + test('registers instance', () { + SharedPreferencesMacOS.registerWith(); + expect(SharedPreferencesStorePlatform.instance, + isA()); + }); + + test('getAll', () async { + store = SharedPreferencesMacOS(); + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await store.getAll(), kTestValues); + expect(log.single.method, 'getAll'); + }); + + test('remove', () async { + store = SharedPreferencesMacOS(); + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await store.remove('flutter.String'), true); + expect(await store.remove('flutter.Bool'), true); + expect(await store.remove('flutter.Int'), true); + expect(await store.remove('flutter.Double'), true); + expect(await testData.getAll(), { + 'flutter.StringList': ['foo', 'bar'], + }); + + expect(log, hasLength(4)); + for (final MethodCall call in log) { + expect(call.method, 'remove'); + } + }); + + test('setValue', () async { + store = SharedPreferencesMacOS(); + expect(await testData.getAll(), isEmpty); + for (final String key in kTestValues.keys) { + final Object value = kTestValues[key]!; + expect(await store.setValue(key.split('.').last, key, value), true); + } + expect(await testData.getAll(), kTestValues); + + expect(log, hasLength(5)); + expect(log[0].method, 'setString'); + expect(log[1].method, 'setBool'); + expect(log[2].method, 'setInt'); + expect(log[3].method, 'setDouble'); + expect(log[4].method, 'setStringList'); + }); + + test('clear', () async { + store = SharedPreferencesMacOS(); + testData = InMemorySharedPreferencesStore.withData(kTestValues); + expect(await testData.getAll(), isNotEmpty); + expect(await store.clear(), true); + expect(await testData.getAll(), isEmpty); + expect(log.single.method, 'clear'); + }); + }); +} diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index b402f6e57e88..51b59651b49a 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Fixes newly enabled analyzer options. + ## 2.0.0 * Migrate to null safety. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart index fa1bdc097b8d..2974f0a69e1b 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart @@ -43,7 +43,9 @@ class MethodChannelSharedPreferencesStore final Map? preferences = await _kChannel.invokeMapMethod('getAll'); - if (preferences == null) return {}; + if (preferences == null) { + return {}; + } return preferences; } } diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 39f5a3ce42e9..43669d624f2d 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -1,12 +1,12 @@ name: shared_preferences_platform_interface description: A common platform interface for the shared_preferences plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_platform_interface +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 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart index 3b43062c2be3..31ee89e4c3f6 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart @@ -4,8 +4,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -37,7 +37,7 @@ void main() { return await testData.getAll(); } if (methodCall.method == 'remove') { - final String key = methodCall.arguments['key']; + final String key = (methodCall.arguments['key'] as String?)!; return await testData.remove(key); } if (methodCall.method == 'clear') { @@ -47,8 +47,8 @@ void main() { final Match? match = setterRegExp.matchAsPrefix(methodCall.method); if (match?.groupCount == 1) { final String valueType = match!.group(1)!; - final String key = methodCall.arguments['key']; - final Object value = methodCall.arguments['value']; + final String key = (methodCall.arguments['key'] as String?)!; + final Object value = (methodCall.arguments['value'] as Object?)!; return await testData.setValue(valueType, key, value); } fail('Unexpected method call: ${methodCall.method}'); @@ -78,15 +78,15 @@ void main() { }); expect(log, hasLength(4)); - for (MethodCall call in log) { + for (final MethodCall call in log) { expect(call.method, 'remove'); } }); test('setValue', () async { expect(await testData.getAll(), isEmpty); - for (String key in kTestValues.keys) { - final dynamic value = kTestValues[key]; + for (final String key in kTestValues.keys) { + final Object value = kTestValues[key]!; expect(await store.setValue(key.split('.').last, key, value), true); } expect(await testData.getAll(), kTestValues); diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index dd68f5321541..9ea249034105 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,13 @@ +## 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. +* Removes dependency on `meta`. + ## 2.0.2 * Add `implements` to pubspec. diff --git a/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart index d95a0512615e..d3bfa49af8a0 100644 --- a/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart @@ -62,12 +62,12 @@ void main() { testWidgets('setValue', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); - for (String key in kTestValues.keys) { + for (final String key in kTestValues.keys) { final dynamic value = kTestValues[key]; expect(await store.setValue(key.split('.').last, key, value), true); } expect(html.window.localStorage.keys, hasLength(kTestValues.length)); - for (String key in html.window.localStorage.keys) { + for (final String key in html.window.localStorage.keys) { expect(html.window.localStorage[key], json.encode(kTestValues[key])); } 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 e1a38dcdcd46..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,19 +5,22 @@ 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 { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml index a83a71b40bf8..656fdeb01876 100644 --- a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -3,19 +3,19 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.8.0" dependencies: - shared_preferences_web: - path: ../ flutter: sdk: flutter + shared_preferences_web: + path: ../ dev_dependencies: - js: ^0.6.3 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter + js: ^0.6.3 diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart index 9cff1d448896..d9d623465188 100644 --- a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart +++ b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart @@ -23,16 +23,14 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { // IMPORTANT: Do not use html.window.localStorage.clear() as that will // remove _all_ local data, not just the keys prefixed with // "flutter." - for (String key in _storedFlutterKeys) { - html.window.localStorage.remove(key); - } + _storedFlutterKeys.forEach(html.window.localStorage.remove); return true; } @override Future> getAll() async { - final Map allData = {}; - for (String key in _storedFlutterKeys) { + final Map allData = {}; + for (final String key in _storedFlutterKeys) { allData[key] = _decodeValue(html.window.localStorage[key]!); } return allData; @@ -64,7 +62,7 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { Iterable get _storedFlutterKeys { return html.window.localStorage.keys - .where((key) => key.startsWith('flutter.')); + .where((String key) => key.startsWith('flutter.')); } String _encodeValue(Object? value) { @@ -72,7 +70,7 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { } Object _decodeValue(String encodedValue) { - final Object decodedValue = json.decode(encodedValue); + final Object? decodedValue = json.decode(encodedValue); if (decodedValue is List) { // JSON does not preserve generics. The encode/decode roundtrip is @@ -81,6 +79,6 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { return decodedValue.cast(); } - return decodedValue; + return decodedValue!; } } diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index c878903ac236..c50958363d16 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -1,12 +1,12 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences -repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web +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.2 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -21,7 +21,6 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.3.0 shared_preferences_platform_interface: ^2.0.0 dev_dependencies: diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 7502ec917d80..f79f9e3d5d39 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,21 @@ +## 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`. + +## 2.0.4 + +* Removes dependency on `meta`. + +## 2.0.3 + +* Removed obsolete `pluginClass: none` from pubpsec. +* Fixes newly enabled analyzer options. + ## 2.0.2 * Updated installation instructions in README. diff --git a/packages/shared_preferences/shared_preferences_windows/example/README.md b/packages/shared_preferences/shared_preferences_windows/example/README.md index d85bb4107622..30c7f7e50c3b 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/README.md +++ b/packages/shared_preferences/shared_preferences_windows/example/README.md @@ -1,16 +1,3 @@ # shared_preferences_windows_example Demonstrates how to use the shared_preferences_windows 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/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart index 207d712650a7..92a34fc2a255 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart @@ -2,16 +2,15 @@ // 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:shared_preferences_windows/shared_preferences_windows.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesWindows', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -19,7 +18,7 @@ void main() { 'flutter.List': ['foo', 'bar'], }; - const Map kTestValues2 = { + const Map kTestValues2 = { 'flutter.String': 'goodbye world', 'flutter.bool': false, 'flutter.int': 1337, @@ -27,17 +26,9 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - late SharedPreferencesWindows preferences; - - setUp(() async { - preferences = SharedPreferencesWindows.instance; - }); - - tearDown(() { - preferences.clear(); - }); - testWidgets('reading', (WidgetTester _) async { + final SharedPreferencesWindows preferences = SharedPreferencesWindows(); + preferences.clear(); final Map values = await preferences.getAll(); expect(values['String'], isNull); expect(values['bool'], isNull); @@ -47,15 +38,16 @@ void main() { }); testWidgets('writing', (WidgetTester _) async { - await Future.wait(>[ - preferences.setValue( - 'String', 'String', kTestValues2['flutter.String']), - preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), - preferences.setValue('Int', 'int', kTestValues2['flutter.int']), - preferences.setValue( - 'Double', 'double', kTestValues2['flutter.double']), - preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) - ]); + final SharedPreferencesWindows preferences = SharedPreferencesWindows(); + preferences.clear(); + await preferences.setValue( + 'String', 'String', kTestValues2['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues2['flutter.int']!); + await preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']!); + await preferences.setValue( + 'StringList', 'List', kTestValues2['flutter.List']!); final Map values = await preferences.getAll(); expect(values['String'], kTestValues2['flutter.String']); expect(values['bool'], kTestValues2['flutter.bool']); @@ -65,27 +57,31 @@ void main() { }); testWidgets('removing', (WidgetTester _) async { + final SharedPreferencesWindows preferences = SharedPreferencesWindows(); + preferences.clear(); const String key = 'testKey'; - await preferences.setValue('String', key, kTestValues['flutter.String']); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']); - await preferences.setValue('Int', key, kTestValues['flutter.int']); - await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue('String', key, kTestValues['flutter.String']!); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); + await preferences.setValue('Int', key, kTestValues['flutter.int']!); + await preferences.setValue('Double', key, kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']); + 'StringList', key, kTestValues['flutter.List']!); await preferences.remove(key); final Map values = await preferences.getAll(); expect(values[key], isNull); }); testWidgets('clearing', (WidgetTester _) async { + final SharedPreferencesWindows preferences = SharedPreferencesWindows(); + preferences.clear(); await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + 'String', 'String', kTestValues['flutter.String']!); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']); + 'Double', 'double', kTestValues['flutter.double']!); await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']); + 'StringList', 'List', kTestValues['flutter.List']!); await preferences.clear(); final Map values = await preferences.getAll(); expect(values['String'], null); 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 0cdd37394706..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,13 +10,15 @@ 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 MaterialApp( + return const MaterialApp( title: 'SharedPreferences Demo', home: SharedPreferencesDemo(), ); @@ -24,18 +26,18 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key? key}) : super(key: key); + const SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - final prefs = SharedPreferencesWindows.instance; + final SharedPreferencesWindows prefs = SharedPreferencesWindows(); late Future _counter; Future _incrementCounter() async { - final values = await prefs.getAll(); + final Map values = await prefs.getAll(); final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { @@ -49,7 +51,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] as int? ?? 0); + return values['counter'] as int? ?? 0; }); } @@ -57,7 +59,7 @@ class SharedPreferencesDemoState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("SharedPreferences Demo"), + title: const Text('SharedPreferences Demo'), ), body: Center( child: FutureBuilder( diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index 96762e933a9d..c7a0eb82cc07 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.8.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680af38..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp index 126302b0be18..c7dbde1c7123 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp @@ -11,7 +11,7 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _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()) { diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.cpp b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.cpp index 537728149601..e875ce8b05a9 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.cpp +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.cpp @@ -13,7 +13,7 @@ void CreateAndAttachConsole() { if (::AllocConsole()) { - FILE *unused; + FILE* unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } diff --git a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart index b8cd3702b837..a60d2ed09926 100644 --- a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart +++ b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -4,30 +4,31 @@ import 'dart:async'; import 'dart:convert' show json; + import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:path/path.dart' as path; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; /// The Windows implementation of [SharedPreferencesStorePlatform]. /// /// This class implements the `package:shared_preferences` functionality for Windows. class SharedPreferencesWindows extends SharedPreferencesStorePlatform { - /// The default instance of [SharedPreferencesWindows] to use. - /// TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. - /// https://github.com/flutter/flutter/issues/81421 + /// Deprecated instance of [SharedPreferencesWindows]. + /// Use [SharedPreferencesStorePlatform.instance] instead. + @Deprecated('Use `SharedPreferencesStorePlatform.instance` instead.') static SharedPreferencesWindows instance = SharedPreferencesWindows(); /// Registers the Windows implementation. static void registerWith() { - SharedPreferencesStorePlatform.instance = instance; + SharedPreferencesStorePlatform.instance = SharedPreferencesWindows(); } /// File system used to store to disk. Exposed for testing only. @visibleForTesting - FileSystem fs = LocalFileSystem(); + FileSystem fs = const LocalFileSystem(); /// The path_provider_windows instance used to find the support directory. @visibleForTesting @@ -44,7 +45,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { if (_localDataFilePath != null) { return _localDataFilePath!; } - final directory = await pathProvider.getApplicationSupportPath(); + final String? directory = await pathProvider.getApplicationSupportPath(); if (directory == null) { return null; } @@ -58,12 +59,15 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { if (_cachedPreferences != null) { return _cachedPreferences!; } - Map preferences = {}; + Map preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile != null && localDataFile.existsSync()) { - String stringMap = localDataFile.readAsStringSync(); + final String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { - preferences = json.decode(stringMap).cast(); + final Object? data = json.decode(stringMap); + if (data is Map) { + preferences = data.cast(); + } } } _cachedPreferences = preferences; @@ -76,16 +80,16 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { try { final File? localDataFile = await _getLocalDataFile(); if (localDataFile == null) { - print("Unable to determine where to write preferences."); + print('Unable to determine where to write preferences.'); return false; } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } - String stringMap = json.encode(preferences); + final String stringMap = json.encode(preferences); localDataFile.writeAsStringSync(stringMap); } catch (e) { - print("Error saving preferences to disk: $e"); + print('Error saving preferences to disk: $e'); return false; } return true; @@ -93,7 +97,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { @override Future clear() async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.clear(); return _writePreferences(preferences); } @@ -105,14 +109,14 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { @override Future remove(String key) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences.remove(key); return _writePreferences(preferences); } @override Future setValue(String valueType, String key, Object value) async { - var preferences = await _readPreferences(); + final Map preferences = await _readPreferences(); preferences[key] = value; return _writePreferences(preferences); } diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 87b685f6d0bc..57e086b81ed3 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -1,12 +1,12 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences -repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows +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.0.2 +version: 2.1.1 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,13 +14,11 @@ flutter: platforms: windows: dartPluginClass: SharedPreferencesWindows - pluginClass: none dependencies: + file: ^6.0.0 flutter: sdk: flutter - file: ^6.0.0 - meta: ^1.3.0 path: ^1.8.0 path_provider_platform_interface: ^2.0.0 path_provider_windows: ^2.0.0 @@ -29,4 +27,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart index 6bb21b814e07..0c47e9865765 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 @@ -20,11 +20,11 @@ void main() { }); Future _getFilePath() async { - final directory = await pathProvider.getApplicationSupportPath(); + final String? directory = await pathProvider.getApplicationSupportPath(); return path.join(directory!, 'shared_preferences.json'); } - _writeTestFile(String value) async { + Future _writeTestFile(String value) async { fileSystem.file(await _getFilePath()) ..createSync(recursive: true) ..writeAsStringSync(value); @@ -35,7 +35,7 @@ void main() { } SharedPreferencesWindows _getPreferences() { - var prefs = SharedPreferencesWindows(); + final SharedPreferencesWindows prefs = SharedPreferencesWindows(); prefs.fs = fileSystem; prefs.pathProvider = pathProvider; return prefs; @@ -49,9 +49,9 @@ void main() { test('getAll', () async { await _writeTestFile('{"key1": "one", "key2": 2}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); - var values = await prefs.getAll(); + final Map values = await prefs.getAll(); expect(values, hasLength(2)); expect(values['key1'], 'one'); expect(values['key2'], 2); @@ -59,7 +59,7 @@ void main() { test('remove', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); await prefs.remove('key2'); @@ -68,7 +68,7 @@ void main() { test('setValue', () async { await _writeTestFile('{}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); await prefs.setValue('', 'key1', 'one'); await prefs.setValue('', 'key2', 2); @@ -78,7 +78,7 @@ void main() { test('clear', () async { await _writeTestFile('{"key1":"one","key2":2}'); - var prefs = _getPreferences(); + final SharedPreferencesWindows prefs = _getPreferences(); await prefs.clear(); expect(await _readTestFile(), '{}'); @@ -92,6 +92,7 @@ void main() { /// path it returns is a root path that does not actually exist on Windows. class FakePathProviderWindows extends PathProviderPlatform implements PathProviderWindows { + @override late VersionInfoQuerier versionInfoQuerier; @override diff --git a/packages/url_launcher/analysis_options.yaml b/packages/url_launcher/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/url_launcher/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 4b52a8d46f8b..c6e4b9aed8b3 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,87 @@ +## 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. + +## 6.0.19 + +* Updates README: + * Adds description for `file` scheme usage. + * Updates `Uri` class link to SDK documentation. + +## 6.0.18 + +* Removes dependency on `meta`. + +## 6.0.17 + +* Updates code for new analysis options. + +## 6.0.16 + +* Moves Android and iOS implementations to federated packages. + +## 6.0.15 + +* Updates README: + * Improves organization. + * Clarifies how `canLaunch` should be used. +* Updates example application to demonstrate intended use of `canLaunch`. + +## 6.0.14 + +* Updates readme to indicate that sending SMS messages on Android 11 requires to add a query to AndroidManifest.xml. +* Fixes integration tests. +* Updates example app Android compileSdkVersion to 31. + +## 6.0.13 + +* Fixed extracting browser headers when they are null error. + +## 6.0.12 + +* Fixed an error where 'launch' method of url_launcher would cause an error if the provided URL was not valid by RFC 3986. + ## 6.0.11 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index c649b5c0fe7b..e9e4dae476cc 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -1,43 +1,34 @@ + + # 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. - -## Usage -To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). - -## Installation +A Flutter plugin for launching a URL. -### iOS -Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Windows 10+ | -Example: -``` -LSApplicationQueriesSchemes - - https - http - -``` +## Usage -See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl) for more details. +To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). ### Example + ``` dart import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -const _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'), ), ), @@ -45,113 +36,184 @@ void main() => runApp( ), ); -void _launchURL() async => - await canLaunch(_url) ? await launch(_url) : throw 'Could not launch $_url'; +Future _launchUrl() async { + if (!await launchUrl(_url)) { + throw 'Could not launch $_url'; + } +} +``` + +See the example app for more complex examples. + +## Configuration + +### iOS +Add any URL schemes passed to `canLaunchUrl` as `LSApplicationQueriesSchemes` +entries in your Info.plist file, otherwise it will return false. + +Example: +```xml +LSApplicationQueriesSchemes + + sms + tel + ``` +See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl) for more details. + ### 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. -Common schemes supported by both iOS and Android: +Commonly used schemes include: -| Scheme | Action | -|---|---| -| `http:` , `https:`, e.g. `http://flutter.dev` | Open URL in the default browser | -| `mailto:?subject=&body=`, e.g. `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to in the default email app | -| `tel:`, e.g. `tel:+1 555 010 999` | Make a phone call to using the default phone app | -| `sms:`, e.g. `sms:5550101234` | Send an SMS message to using the default messaging app | +| Scheme | Example | Action | +|:---|:---|:---| +| `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) +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 -[`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html). -For example: +characters. In general this is handled automatically by the +[`Uri` class](https://api.dart.dev/dart-core/Uri-class.html). + +**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); +``` + +Encoding for `sms` is slightly different: -final Uri emailLaunchUri = Uri( - scheme: 'mailto', - path: 'smith@example.com', - query: encodeQueryParameters({ - 'subject': 'Example Subject & Symbols are allowed!' - }), + +```dart +final Uri smsLaunchUri = Uri( + scheme: 'sms', + path: '0118 999 881 999 119 7253', + queryParameters: { + 'body': Uri.encodeComponent('Example Subject & Symbols are allowed!'), + }, ); +``` -launch(emailLaunchUri.toString()); +### URLs not handled by `Uri` + +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`. + +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. + +### File scheme handling + +`file:` scheme can be used on desktop platforms: Windows, macOS, and Linux. + +We recommend checking first whether the directory or file exists before calling `launchUrl`. + +Example: + + +```dart +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'; +} ``` -**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. +#### macOS file access configuration + +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 -## Handling missing URL receivers - -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 `email` scheme. - -We recommend checking which URL schemes are supported using the -[`canLaunch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunch.html) -method prior to calling `launch`. If the `canLaunch` method returns false, as a -best practice we suggest adjusting the application UI so that the unsupported -URL is never triggered; for example, if the `email` scheme is not supported, a -UI button that would have sent email can be changed to redirect the user to a -web page using a URL following the `http` scheme. - -## 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. +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/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle deleted file mode 100644 index d374d40534c3..000000000000 --- a/packages/url_launcher/url_launcher/android/build.gradle +++ /dev/null @@ -1,56 +0,0 @@ -group 'io.flutter.plugins.urllauncher' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -dependencies { - compileOnly 'androidx.annotation:annotation:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' - testImplementation 'androidx.test:core:1.0.0' - testImplementation 'org.robolectric:robolectric:4.3' -} diff --git a/packages/url_launcher/url_launcher/android/settings.gradle b/packages/url_launcher/url_launcher/android/settings.gradle deleted file mode 100644 index 6620cd7dfb8b..000000000000 --- a/packages/url_launcher/url_launcher/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'url_launcher' diff --git a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java deleted file mode 100644 index 9e798abcdbb5..000000000000 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java +++ /dev/null @@ -1,122 +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.urllauncher; - -import android.os.Bundle; -import android.util.Log; -import androidx.annotation.Nullable; -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.plugins.urllauncher.UrlLauncher.LaunchStatus; -import java.util.Map; - -/** - * Translates incoming UrlLauncher MethodCalls into well formed Java function calls for {@link - * UrlLauncher}. - */ -final class MethodCallHandlerImpl implements MethodCallHandler { - private static final String TAG = "MethodCallHandlerImpl"; - private final UrlLauncher urlLauncher; - @Nullable private MethodChannel channel; - - /** Forwards all incoming MethodChannel calls to the given {@code urlLauncher}. */ - MethodCallHandlerImpl(UrlLauncher urlLauncher) { - this.urlLauncher = urlLauncher; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - final String url = call.argument("url"); - switch (call.method) { - case "canLaunch": - onCanLaunch(result, url); - break; - case "launch": - onLaunch(call, result, url); - break; - case "closeWebView": - onCloseWebView(result); - break; - default: - result.notImplemented(); - break; - } - } - - /** - * Registers this instance as a method call handler on the given {@code messenger}. - * - *

Stops any previously started and unstopped calls. - * - *

This should be cleaned with {@link #stopListening} once the messenger is disposed of. - */ - void startListening(BinaryMessenger messenger) { - if (channel != null) { - Log.wtf(TAG, "Setting a method call handler before the last was disposed."); - stopListening(); - } - - channel = new MethodChannel(messenger, "plugins.flutter.io/url_launcher"); - channel.setMethodCallHandler(this); - } - - /** - * Clears this instance from listening to method calls. - * - *

Does nothing if {@link #startListening} hasn't been called, or if we're already stopped. - */ - void stopListening() { - if (channel == null) { - Log.d(TAG, "Tried to stop listening when no MethodChannel had been initialized."); - return; - } - - channel.setMethodCallHandler(null); - channel = null; - } - - private void onCanLaunch(Result result, String url) { - result.success(urlLauncher.canLaunch(url)); - } - - private void onLaunch(MethodCall call, Result result, String url) { - final boolean useWebView = call.argument("useWebView"); - final boolean enableJavaScript = call.argument("enableJavaScript"); - final boolean enableDomStorage = call.argument("enableDomStorage"); - final Map headersMap = call.argument("headers"); - final Bundle headersBundle = extractBundle(headersMap); - - LaunchStatus launchStatus = - urlLauncher.launch(url, headersBundle, useWebView, enableJavaScript, enableDomStorage); - - if (launchStatus == LaunchStatus.NO_ACTIVITY) { - result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); - } else if (launchStatus == LaunchStatus.ACTIVITY_NOT_FOUND) { - result.error( - "ACTIVITY_NOT_FOUND", - String.format("No Activity found to handle intent { %s }", url), - null); - } else { - result.success(true); - } - } - - private void onCloseWebView(Result result) { - urlLauncher.closeWebView(); - result.success(null); - } - - private static Bundle extractBundle(Map headersMap) { - final Bundle headersBundle = new Bundle(); - for (String key : headersMap.keySet()) { - final String value = headersMap.get(key); - headersBundle.putString(key, value); - } - return headersBundle; - } -} diff --git a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java b/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java deleted file mode 100644 index 5e0811399ac6..000000000000 --- a/packages/url_launcher/url_launcher/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java +++ /dev/null @@ -1,217 +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.urllauncher; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.os.Bundle; -import androidx.test.core.app.ApplicationProvider; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel.Result; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class MethodCallHandlerImplTest { - private static final String CHANNEL_NAME = "plugins.flutter.io/url_launcher"; - private UrlLauncher urlLauncher; - private MethodCallHandlerImpl methodCallHandler; - - @Before - public void setUp() { - urlLauncher = new UrlLauncher(ApplicationProvider.getApplicationContext(), /*activity=*/ null); - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - } - - @Test - public void startListening_registersChannel() { - BinaryMessenger messenger = mock(BinaryMessenger.class); - - methodCallHandler.startListening(messenger); - - verify(messenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - } - - @Test - public void startListening_unregistersExistingChannel() { - BinaryMessenger firstMessenger = mock(BinaryMessenger.class); - BinaryMessenger secondMessenger = mock(BinaryMessenger.class); - methodCallHandler.startListening(firstMessenger); - - methodCallHandler.startListening(secondMessenger); - - // Unregisters the first and then registers the second. - verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - verify(secondMessenger, times(1)) - .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); - } - - @Test - public void stopListening_unregistersExistingChannel() { - BinaryMessenger messenger = mock(BinaryMessenger.class); - methodCallHandler.startListening(messenger); - - methodCallHandler.stopListening(); - - verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); - } - - @Test - public void stopListening_doesNothingWhenUnset() { - BinaryMessenger messenger = mock(BinaryMessenger.class); - - methodCallHandler.stopListening(); - - verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); - } - - @Test - public void onMethodCall_canLaunchReturnsTrue() { - urlLauncher = mock(UrlLauncher.class); - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - String url = "foo"; - when(urlLauncher.canLaunch(url)).thenReturn(true); - Result result = mock(Result.class); - Map args = new HashMap<>(); - args.put("url", url); - - methodCallHandler.onMethodCall(new MethodCall("canLaunch", args), result); - - verify(result, times(1)).success(true); - } - - @Test - public void onMethodCall_canLaunchReturnsFalse() { - urlLauncher = mock(UrlLauncher.class); - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - String url = "foo"; - when(urlLauncher.canLaunch(url)).thenReturn(false); - Result result = mock(Result.class); - Map args = new HashMap<>(); - args.put("url", url); - - methodCallHandler.onMethodCall(new MethodCall("canLaunch", args), result); - - verify(result, times(1)).success(false); - } - - @Test - public void onMethodCall_launchReturnsNoActivityError() { - // Setup mock objects - urlLauncher = mock(UrlLauncher.class); - Result result = mock(Result.class); - // Setup expected values - String url = "foo"; - boolean useWebView = false; - boolean enableJavaScript = false; - boolean enableDomStorage = false; - // Setup arguments map send on the method channel - Map args = new HashMap<>(); - args.put("url", url); - args.put("useWebView", useWebView); - args.put("enableJavaScript", enableJavaScript); - args.put("enableDomStorage", enableDomStorage); - args.put("headers", new HashMap<>()); - // Mock the launch method on the urlLauncher class - when(urlLauncher.launch( - eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage))) - .thenReturn(UrlLauncher.LaunchStatus.NO_ACTIVITY); - // Act by calling the "launch" method on the method channel - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - // Verify the results and assert - verify(result, times(1)) - .error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); - } - - @Test - public void onMethodCall_launchReturnsActivityNotFoundError() { - // Setup mock objects - urlLauncher = mock(UrlLauncher.class); - Result result = mock(Result.class); - // Setup expected values - String url = "foo"; - boolean useWebView = false; - boolean enableJavaScript = false; - boolean enableDomStorage = false; - // Setup arguments map send on the method channel - Map args = new HashMap<>(); - args.put("url", url); - args.put("useWebView", useWebView); - args.put("enableJavaScript", enableJavaScript); - args.put("enableDomStorage", enableDomStorage); - args.put("headers", new HashMap<>()); - // Mock the launch method on the urlLauncher class - when(urlLauncher.launch( - eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage))) - .thenReturn(UrlLauncher.LaunchStatus.ACTIVITY_NOT_FOUND); - // Act by calling the "launch" method on the method channel - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - // Verify the results and assert - verify(result, times(1)) - .error( - "ACTIVITY_NOT_FOUND", - String.format("No Activity found to handle intent { %s }", url), - null); - } - - @Test - public void onMethodCall_launchReturnsTrue() { - // Setup mock objects - urlLauncher = mock(UrlLauncher.class); - Result result = mock(Result.class); - // Setup expected values - String url = "foo"; - boolean useWebView = false; - boolean enableJavaScript = false; - boolean enableDomStorage = false; - // Setup arguments map send on the method channel - Map args = new HashMap<>(); - args.put("url", url); - args.put("useWebView", useWebView); - args.put("enableJavaScript", enableJavaScript); - args.put("enableDomStorage", enableDomStorage); - args.put("headers", new HashMap<>()); - // Mock the launch method on the urlLauncher class - when(urlLauncher.launch( - eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage))) - .thenReturn(UrlLauncher.LaunchStatus.OK); - // Act by calling the "launch" method on the method channel - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - methodCallHandler.onMethodCall(new MethodCall("launch", args), result); - // Verify the results and assert - verify(result, times(1)).success(true); - } - - @Test - public void onMethodCall_closeWebView() { - urlLauncher = mock(UrlLauncher.class); - methodCallHandler = new MethodCallHandlerImpl(urlLauncher); - String url = "foo"; - when(urlLauncher.canLaunch(url)).thenReturn(true); - Result result = mock(Result.class); - Map args = new HashMap<>(); - args.put("url", url); - - methodCallHandler.onMethodCall(new MethodCall("closeWebView", args), result); - - verify(urlLauncher, times(1)).closeWebView(); - verify(result, times(1)).success(null); - } -} 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 8280da86f124..8c7e84563ee6 100644 --- a/packages/url_launcher/url_launcher/example/android/app/build.gradle +++ b/packages/url_launcher/url_launcher/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 30 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -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/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTest/java/io/flutter/plugins/urllauncherexample/FlutterActivityTest.java similarity index 100% rename from packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java rename to packages/url_launcher/url_launcher/example/android/app/src/androidTest/java/io/flutter/plugins/urllauncherexample/FlutterActivityTest.java 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 918c29ee2dca..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,17 +7,29 @@ --> + + - + + - + + + + + + + + + /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; }; - DD4687403C4F35FCD2994FDE /* [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 */ @@ -418,37 +270,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7151F4426604CFB0028CB91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F7151F5226604D060028CB91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - F7151F4E26604CFB0028CB91 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F7151F4D26604CFB0028CB91 /* PBXContainerItemProxy */; - }; - F7151F5C26604D060028CB91 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F7151F5B26604D060028CB91 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -617,62 +440,6 @@ }; name = Release; }; - F7151F4F26604CFB0028CB91 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 666BCD7C181C34F8BE58929B /* 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"; - 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; - }; - F7151F5026604CFB0028CB91 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D25C434271ACF6555E002440 /* 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"; - 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; - }; - F7151F5E26604D060028CB91 /* Debug */ = { - isa = XCBuildConfiguration; - 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; - }; - F7151F5F26604D060028CB91 /* Release */ = { - isa = XCBuildConfiguration; - 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 */ @@ -694,24 +461,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F7151F5126604CFB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7151F4F26604CFB0028CB91 /* Debug */, - F7151F5026604CFB0028CB91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F7151F5D26604D060028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7151F5E26604D060028CB91 /* Debug */, - F7151F5F26604D060028CB91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/url_launcher/url_launcher/example/ios/Runner/main.m b/packages/url_launcher/url_launcher/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner/main.m +++ b/packages/url_launcher/url_launcher/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/url_launcher/url_launcher/example/lib/basic.dart b/packages/url_launcher/url_launcher/example/lib/basic.dart new file mode 100644 index 000000000000..987ca2134318 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/lib/basic.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. + +// Run this example with: flutter run -t lib/basic.dart -d emulator + +// This file is used to extract code samples for the README.md file. +// Run update-excerpts if you modify this file. + +// #docregion basic-example +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +final Uri _url = Uri.parse('https://flutter.dev'); + +void main() => 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 d593e6d5e001..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( @@ -22,88 +24,85 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'URL Launcher'), + home: const MyHomePage(title: 'URL Launcher'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { + bool _hasCallSupport = false; Future? _launched; String _phone = ''; - Future _launchInBrowser(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { + @override + void initState() { + super.initState(); + // Check for phone call support. + canLaunchUrl(Uri(scheme: 'tel', path: '123')).then((bool result) { + setState(() { + _hasCallSupport = result; + }); + }); + } + + Future _launchInBrowser(Uri url) async { + if (!await launchUrl( + url, + mode: LaunchMode.externalApplication, + )) { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { + Future _launchInWebViewOrVC(Uri url) async { + if (!await launchUrl( + url, + 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 canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, - ); - } else { + Future _launchInWebViewWithoutJavaScript(Uri url) async { + if (!await launchUrl( + url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration(enableJavaScript: false), + )) { throw 'Could not launch $url'; } } - Future _launchInWebViewWithDomStorage(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, - ); - } else { + Future _launchInWebViewWithoutDomStorage(Uri url) async { + if (!await launchUrl( + url, + mode: LaunchMode.inAppWebView, + webViewConfiguration: const WebViewConfiguration(enableDomStorage: false), + )) { throw 'Could not launch $url'; } } - Future _launchUniversalLinkIos(String url) async { - if (await canLaunch(url)) { - final bool nativeAppLaunchSucceeded = await launch( + Future _launchUniversalLinkIos(Uri url) async { + final bool nativeAppLaunchSucceeded = await launchUrl( + url, + mode: LaunchMode.externalNonBrowserApplication, + ); + if (!nativeAppLaunchSucceeded) { + await launchUrl( url, - forceSafariVC: false, - universalLinksOnly: true, + mode: LaunchMode.inAppWebView, ); - if (!nativeAppLaunchSucceeded) { - await launch( - url, - forceSafariVC: true, - ); - } } } @@ -115,17 +114,20 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } + Future _makePhoneCall(String phoneNumber) async { + final Uri launchUri = Uri( + scheme: 'tel', + path: phoneNumber, + ); + await launchUrl(launchUri); } @override Widget build(BuildContext context) { - const String toLaunch = 'https://www.cylog.org/headers/'; + // onPressed calls using this URL are not gated on a 'canLaunch' check + // because the assumption is that every device can launch a web URL. + final Uri toLaunch = + Uri(scheme: 'https', host: 'www.cylog.org', path: 'headers/'); return Scaffold( appBar: AppBar( title: Text(widget.title), @@ -143,14 +145,18 @@ 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), - child: Text(toLaunch), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(toLaunch.toString()), ), ElevatedButton( onPressed: () => setState(() { @@ -167,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( @@ -191,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'), @@ -201,11 +207,11 @@ class _MyHomePageState extends State { uri: Uri.parse( 'https://pub.dev/documentation/url_launcher/latest/link/link-library.html'), target: LinkTarget.blank, - builder: (ctx, openLink) { + builder: (BuildContext ctx, FollowLink? openLink) { return TextButton.icon( onPressed: openLink, - label: Text('Link Widget documentation'), - icon: Icon(Icons.read_more), + label: const Text('Link Widget documentation'), + icon: const Icon(Icons.read_more), ); }, ), diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt index 94f43ff7fa6a..33fd5801e713 100644 --- a/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt +++ b/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt @@ -78,7 +78,8 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - linux-x64 ${CMAKE_BUILD_TYPE} + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index f6f23bfe970f..000000000000 --- a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ 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/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/url_launcher/url_launcher/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 8236f5728c63..000000000000 --- a/packages/url_launcher/url_launcher/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import url_launcher_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) -} diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index db1d548695dc..673785e9e1f6 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.8.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,11 +19,11 @@ dependencies: path: ../ dev_dependencies: - integration_test: - sdk: flutter + build_runner: ^2.1.10 flutter_driver: sdk: flutter - pedantic: ^1.10.0 + integration_test: + sdk: flutter mockito: ^5.0.0 plugin_platform_interface: ^2.0.0 diff --git a/packages/url_launcher/url_launcher/example/url_launcher_example.iml b/packages/url_launcher/url_launcher/example/url_launcher_example.iml deleted file mode 100644 index f2b096d12510..000000000000 --- a/packages/url_launcher/url_launcher/example/url_launcher_example.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index d9fdd53925c5..000000000000 --- a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherPlugin")); -} diff --git a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ 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/example/windows/runner/main.cpp b/packages/url_launcher/url_launcher/example/windows/runner/main.cpp index 126302b0be18..c7dbde1c7123 100644 --- a/packages/url_launcher/url_launcher/example/windows/runner/main.cpp +++ b/packages/url_launcher/url_launcher/example/windows/runner/main.cpp @@ -11,7 +11,7 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _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()) { diff --git a/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp b/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp index 537728149601..e875ce8b05a9 100644 --- a/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp +++ b/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp @@ -13,7 +13,7 @@ void CreateAndAttachConsole() { if (::AllocConsole()) { - FILE *unused; + FILE* unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } diff --git a/packages/url_launcher/url_launcher/ios/Assets/.gitkeep b/packages/url_launcher/url_launcher/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/url_launcher/url_launcher/ios/url_launcher.podspec b/packages/url_launcher/url_launcher/ios/url_launcher.podspec deleted file mode 100644 index 6af64b66bee4..000000000000 --- a/packages/url_launcher/url_launcher/ios/url_launcher.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'url_launcher' - s.version = '0.0.1' - s.summary = 'Flutter plugin for launching a URL.' - s.description = <<-DESC -A Flutter plugin for making the underlying platform (Android or iOS) launch a URL. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/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' } - s.documentation_url = 'https://pub.dev/packages/url_launcher' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/url_launcher/url_launcher/lib/link.dart b/packages/url_launcher/url_launcher/lib/link.dart index 12a213b62761..00947cd4e22f 100644 --- a/packages/url_launcher/url_launcher/lib/link.dart +++ b/packages/url_launcher/url_launcher/lib/link.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/link.dart' show Link; export 'package:url_launcher_platform_interface/link.dart' show FollowLink, LinkTarget, LinkWidgetBuilder; + +export 'src/link.dart' show Link; 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 72d6e247c970..8c0c18e820e3 100644 --- a/packages/url_launcher/url_launcher/lib/src/link.dart +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -6,11 +6,12 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.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 = @@ -43,27 +44,31 @@ Future Function(Object?, String) pushRouteToFrameworkFunction = /// ); /// ``` class Link extends StatelessWidget implements LinkInfo { + /// Creates a widget that renders a real link on the web, and uses WebViews in + /// native platforms to open links. + const Link({ + Key? key, + required this.uri, + this.target = LinkTarget.defaultTarget, + required this.builder, + }) : super(key: key); + /// Called at build time to construct the widget tree under the link. + @override final LinkWidgetBuilder builder; /// The destination that this link leads to. + @override final Uri? uri; /// The target indicating where to open the link. + @override final LinkTarget target; /// Whether the link is disabled or not. + @override bool get isDisabled => uri == null; - /// Creates a widget that renders a real link on the web, and uses WebViews in - /// native platforms to open links. - Link({ - Key? key, - required this.uri, - this.target = LinkTarget.defaultTarget, - required this.builder, - }) : super(key: key); - LinkDelegate get _effectiveDelegate { return UrlLauncherPlatform.instance.linkDelegate ?? DefaultLinkDelegate.create; @@ -81,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]. /// @@ -94,13 +99,18 @@ class DefaultLinkDelegate extends StatelessWidget { final LinkInfo link; bool get _useWebView { - if (link.target == LinkTarget.self) return true; - if (link.target == LinkTarget.blank) return false; + if (link.target == LinkTarget.self) { + return true; + } + if (link.target == LinkTarget.blank) { + return false; + } return false; } 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. @@ -109,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.toString()}', 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..9061b517e0d5 --- /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/url_launcher_string.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.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 239e3c46c480..36c7b60fdacd 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -2,149 +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.parse(urlString.trimLeft()); - final bool isWebURL = 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 Android (from API 30), [canLaunch] will return `false` when the required -/// visibility configuration is not provided in the AndroidManifest.xml file. -/// For more information see the -/// [Package visibility filtering on Android](https://developer.android.com/training/basics/intents/package-visibility) -/// article in the Android documentation or the url_launcher example app's -/// [AndroidManifest.xml's queries element](https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml). -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 c90d2feb08f4..03d0846bf173 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -1,50 +1,46 @@ name: url_launcher description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. -repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher +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.11 +version: 6.1.5 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.urllauncher - pluginClass: UrlLauncherPlugin + default_package: url_launcher_android ios: - pluginClass: FLTURLLauncherPlugin + default_package: url_launcher_ios linux: - default_package: url_laucher_linux + default_package: url_launcher_linux macos: - default_package: url_laucher_macos + default_package: url_launcher_macos web: default_package: url_launcher_web windows: - default_package: url_laucher_windows + default_package: url_launcher_windows dependencies: flutter: sdk: flutter - meta: ^1.3.0 - # The design on https://flutter.dev/go/federated-plugins was to leave - # implementation constraints as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 - url_launcher_linux: ^2.0.0 - url_launcher_macos: ^2.0.0 - url_launcher_platform_interface: ^2.0.3 + url_launcher_android: ^6.0.13 + url_launcher_ios: ^6.0.13 + # Allow either the pure-native or Dart/native hybrid versions of the desktop + # 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.1.0 url_launcher_web: ^2.0.0 - url_launcher_windows: ^2.0.0 + url_launcher_windows: ">=2.0.0 <4.0.0" dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 plugin_platform_interface: ^2.0.0 test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher/test/link_test.dart b/packages/url_launcher/url_launcher/test/link_test.dart index 819f6a370e30..1c3d3e1e2d5b 100644 --- a/packages/url_launcher/url_launcher/test/link_test.dart +++ b/packages/url_launcher/url_launcher/test/link_test.dart @@ -3,13 +3,13 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; 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, ) @@ -118,11 +116,11 @@ void main() { )); bool frameworkCalled = false; - Future Function(Object?, String) originalPushFunction = + final Future Function(Object?, String) originalPushFunction = pushRouteToFrameworkFunction; pushRouteToFrameworkFunction = (Object? _, String __) { frameworkCalled = true; - return Future.value(ByteData(0)); + return Future.value(ByteData(0)); }; await followLink!(); 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/src/legacy_api_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart new file mode 100644 index 000000000000..11d7d8f17c09 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart @@ -0,0 +1,330 @@ +// Copyright 2013 The Flutter 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#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/src/legacy_api.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('closeWebView default behavior', () async { + await closeWebView(); + expect(mock.closeWebViewCalled, isTrue); + }); + + group('canLaunch', () { + test('returns true', () async { + mock + ..setCanLaunchExpectations('foo') + ..setResponse(true); + + final bool result = await canLaunch('foo'); + + expect(result, isTrue); + }); + + test('returns false', () async { + mock + ..setCanLaunchExpectations('foo') + ..setResponse(false); + + final bool result = await canLaunch('foo'); + + expect(result, isFalse); + }); + }); + group('launch', () { + test('default behavior', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launch('http://flutter.dev/'), isTrue); + }); + + test('with headers', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {'key': 'value'}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch( + 'http://flutter.dev/', + headers: {'key': 'value'}, + ), + isTrue); + }); + + test('force SafariVC', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launch('http://flutter.dev/', forceSafariVC: true), isTrue); + }); + + test('universal links only', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch('http://flutter.dev/', + forceSafariVC: false, universalLinksOnly: true), + isTrue); + }); + + test('force WebView', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launch('http://flutter.dev/', forceWebView: true), isTrue); + }); + + test('force WebView enable javascript', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch('http://flutter.dev/', + forceWebView: true, enableJavaScript: true), + isTrue); + }); + + test('force WebView enable DOM storage', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch('http://flutter.dev/', + forceWebView: true, enableDomStorage: true), + isTrue); + }); + + test('force SafariVC to false', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launch('http://flutter.dev/', forceSafariVC: false), isTrue); + }); + + test('cannot launch a non-web in webview', () async { + expect(() async => await launch('tel:555-555-5555', forceWebView: true), + throwsA(isA())); + }); + + test('send e-mail', () async { + mock + ..setLaunchExpectations( + url: 'mailto:gmail-noreply@google.com?subject=Hello', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect(await launch('mailto:gmail-noreply@google.com?subject=Hello'), + isTrue); + }); + + test('cannot send e-mail with forceSafariVC: true', () async { + expect( + () async => await launch( + 'mailto:gmail-noreply@google.com?subject=Hello', + forceSafariVC: true), + throwsA(isA())); + }); + + test('cannot send e-mail with forceWebView: true', () async { + expect( + () async => await launch( + 'mailto:gmail-noreply@google.com?subject=Hello', + forceWebView: true), + throwsA(isA())); + }); + + test('controls system UI when changing statusBarBrightness', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + + final TestWidgetsFlutterBinding binding = + _anonymize(TestWidgetsFlutterBinding.ensureInitialized())! + as TestWidgetsFlutterBinding; + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + binding.renderView.automaticSystemUiAdjustment = true; + final Future launchResult = + launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); + + // Should take over control of the automaticSystemUiAdjustment while it's + // pending, then restore it back to normal after the launch finishes. + expect(binding.renderView.automaticSystemUiAdjustment, isFalse); + await launchResult; + expect(binding.renderView.automaticSystemUiAdjustment, isTrue); + }); + + test('sets automaticSystemUiAdjustment to not be null', () async { + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + + final TestWidgetsFlutterBinding binding = + _anonymize(TestWidgetsFlutterBinding.ensureInitialized())! + as TestWidgetsFlutterBinding; + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect(binding.renderView.automaticSystemUiAdjustment, true); + final Future launchResult = + launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); + + // The automaticSystemUiAdjustment should be set before the launch + // and equal to true after the launch result is complete. + expect(binding.renderView.automaticSystemUiAdjustment, true); + await launchResult; + expect(binding.renderView.automaticSystemUiAdjustment, true); + }); + + test('open non-parseable url', () async { + mock + ..setLaunchExpectations( + url: + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1'), + isTrue); + }); + + test('cannot open non-parseable url with forceSafariVC: true', () async { + expect( + () async => await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + forceSafariVC: true), + throwsA(isA())); + }); + + test('cannot open non-parseable url with forceWebView: true', () async { + expect( + () async => await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + forceWebView: true), + throwsA(isA())); + }); + }); +} + +/// This removes the type information from a value so that it can be cast +/// to another type even if that cast is redundant. +/// +/// We use this so that APIs whose type have become more descriptive can still +/// be used on the stable branch where they require a cast. +// TODO(ianh): Remove this once we roll stable in late 2021. +Object? _anonymize(T? value) => value; diff --git a/packages/url_launcher/url_launcher/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..02c0b22903e0 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart @@ -0,0 +1,267 @@ +// Copyright 2013 The Flutter Authors. 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, mode: LaunchMode.platformDefault), + 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, mode: LaunchMode.platformDefault), + 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-parseable 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..e226e591a4ae --- /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, mode: LaunchMode.platformDefault), 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, mode: LaunchMode.platformDefault), 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/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart deleted file mode 100644 index 04f727a57746..000000000000 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ /dev/null @@ -1,293 +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_test/flutter_test.dart'; -import 'package:flutter/foundation.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import 'package:flutter/services.dart' show PlatformException; - -import 'mock_url_launcher_platform.dart'; - -void main() { - final MockUrlLauncher mock = MockUrlLauncher(); - UrlLauncherPlatform.instance = mock; - - test('closeWebView default behavior', () async { - await closeWebView(); - expect(mock.closeWebViewCalled, isTrue); - }); - - group('canLaunch', () { - test('returns true', () async { - mock - ..setCanLaunchExpectations('foo') - ..setResponse(true); - - final bool result = await canLaunch('foo'); - - expect(result, isTrue); - }); - - test('returns false', () async { - mock - ..setCanLaunchExpectations('foo') - ..setResponse(false); - - final bool result = await canLaunch('foo'); - - expect(result, isFalse); - }); - }); - group('launch', () { - test('default behavior', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect(await launch('http://flutter.dev/'), isTrue); - }); - - test('with headers', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {'key': 'value'}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect( - await launch( - 'http://flutter.dev/', - headers: {'key': 'value'}, - ), - isTrue); - }); - - test('force SafariVC', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect(await launch('http://flutter.dev/', forceSafariVC: true), isTrue); - }); - - test('universal links only', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: true, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect( - await launch('http://flutter.dev/', - forceSafariVC: false, universalLinksOnly: true), - isTrue); - }); - - test('force WebView', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect(await launch('http://flutter.dev/', forceWebView: true), isTrue); - }); - - test('force WebView enable javascript', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: true, - enableJavaScript: true, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect( - await launch('http://flutter.dev/', - forceWebView: true, enableJavaScript: true), - isTrue); - }); - - test('force WebView enable DOM storage', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: true, - enableJavaScript: false, - enableDomStorage: true, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect( - await launch('http://flutter.dev/', - forceWebView: true, enableDomStorage: true), - isTrue); - }); - - test('force SafariVC to false', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect(await launch('http://flutter.dev/', forceSafariVC: false), isTrue); - }); - - test('cannot launch a non-web in webview', () async { - expect(() async => await launch('tel:555-555-5555', forceWebView: true), - throwsA(isA())); - }); - - test('send e-mail', () async { - mock - ..setLaunchExpectations( - url: 'mailto:gmail-noreply@google.com?subject=Hello', - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - expect(await launch('mailto:gmail-noreply@google.com?subject=Hello'), - isTrue); - }); - - test('cannot send e-mail with forceSafariVC: true', () async { - expect( - () async => await launch( - 'mailto:gmail-noreply@google.com?subject=Hello', - forceSafariVC: true), - throwsA(isA())); - }); - - test('cannot send e-mail with forceWebView: true', () async { - expect( - () async => await launch( - 'mailto:gmail-noreply@google.com?subject=Hello', - forceWebView: true), - throwsA(isA())); - }); - - test('controls system UI when changing statusBarBrightness', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - - final TestWidgetsFlutterBinding binding = - _anonymize(TestWidgetsFlutterBinding.ensureInitialized()) - as TestWidgetsFlutterBinding; - debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - binding.renderView.automaticSystemUiAdjustment = true; - final Future launchResult = - launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); - - // Should take over control of the automaticSystemUiAdjustment while it's - // pending, then restore it back to normal after the launch finishes. - expect(binding.renderView.automaticSystemUiAdjustment, isFalse); - await launchResult; - expect(binding.renderView.automaticSystemUiAdjustment, isTrue); - }); - - test('sets automaticSystemUiAdjustment to not be null', () async { - mock - ..setLaunchExpectations( - url: 'http://flutter.dev/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: {}, - webOnlyWindowName: null, - ) - ..setResponse(true); - - final TestWidgetsFlutterBinding binding = - _anonymize(TestWidgetsFlutterBinding.ensureInitialized()) - as TestWidgetsFlutterBinding; - debugDefaultTargetPlatformOverride = TargetPlatform.android; - expect(binding.renderView.automaticSystemUiAdjustment, true); - final Future launchResult = - launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); - - // The automaticSystemUiAdjustment should be set before the launch - // and equal to true after the launch result is complete. - expect(binding.renderView.automaticSystemUiAdjustment, true); - await launchResult; - expect(binding.renderView.automaticSystemUiAdjustment, true); - }); - }); -} - -/// This removes the type information from a value so that it can be cast -/// to another type even if that cast is redundant. -/// -/// We use this so that APIs whose type have become more descriptive can still -/// be used on the stable branch where they require a cast. -// TODO(ianh): Remove this once we roll stable in late 2021. -Object? _anonymize(T? value) => value; diff --git a/packages/local_auth/AUTHORS b/packages/url_launcher/url_launcher_android/AUTHORS similarity index 100% rename from packages/local_auth/AUTHORS rename to packages/url_launcher/url_launcher_android/AUTHORS diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md new file mode 100644 index 000000000000..887178c479e4 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -0,0 +1,22 @@ +## 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. + +## 6.0.14 + +* Updates code for new analysis options. +* Removes dependency on `meta`. + +## 6.0.13 + +* Splits from `shared_preferences` as a federated implementation. diff --git a/packages/url_launcher/url_launcher_android/LICENSE b/packages/url_launcher/url_launcher_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_android/README.md b/packages/url_launcher/url_launcher_android/README.md new file mode 100644 index 000000000000..bd3263a30e53 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/README.md @@ -0,0 +1,11 @@ +# url\_launcher\_android + +The Android implementation of [`url_launcher`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `url_launcher` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_android/android/build.gradle b/packages/url_launcher/url_launcher_android/android/build.gradle new file mode 100644 index 000000000000..9c95688eca6d --- /dev/null +++ b/packages/url_launcher/url_launcher_android/android/build.gradle @@ -0,0 +1,56 @@ +group 'io.flutter.plugins.urllauncher' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.4.2' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + compileOnly 'androidx.annotation:annotation:1.0.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'androidx.test:core:1.0.0' + testImplementation 'org.robolectric:robolectric:4.3' +} diff --git a/packages/url_launcher/url_launcher_android/android/settings.gradle b/packages/url_launcher/url_launcher_android/android/settings.gradle new file mode 100644 index 000000000000..d8b7cc47172c --- /dev/null +++ b/packages/url_launcher/url_launcher_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'url_launcher_android' diff --git a/packages/url_launcher/url_launcher/android/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/url_launcher/url_launcher/android/src/main/AndroidManifest.xml rename to packages/url_launcher/url_launcher_android/android/src/main/AndroidManifest.xml diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java new file mode 100644 index 000000000000..f7bed8648872 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java @@ -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. + +package io.flutter.plugins.urllauncher; + +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.Nullable; +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.plugins.urllauncher.UrlLauncher.LaunchStatus; +import java.util.Map; + +/** + * Translates incoming UrlLauncher MethodCalls into well formed Java function calls for {@link + * UrlLauncher}. + */ +final class MethodCallHandlerImpl implements MethodCallHandler { + private static final String TAG = "MethodCallHandlerImpl"; + private final UrlLauncher urlLauncher; + @Nullable private MethodChannel channel; + + /** Forwards all incoming MethodChannel calls to the given {@code urlLauncher}. */ + MethodCallHandlerImpl(UrlLauncher urlLauncher) { + this.urlLauncher = urlLauncher; + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + final String url = call.argument("url"); + switch (call.method) { + case "canLaunch": + onCanLaunch(result, url); + break; + case "launch": + onLaunch(call, result, url); + break; + case "closeWebView": + onCloseWebView(result); + break; + default: + result.notImplemented(); + break; + } + } + + /** + * Registers this instance as a method call handler on the given {@code messenger}. + * + *

Stops any previously started and unstopped calls. + * + *

This should be cleaned with {@link #stopListening} once the messenger is disposed of. + */ + void startListening(BinaryMessenger messenger) { + if (channel != null) { + Log.wtf(TAG, "Setting a method call handler before the last was disposed."); + stopListening(); + } + + channel = new MethodChannel(messenger, "plugins.flutter.io/url_launcher_android"); + channel.setMethodCallHandler(this); + } + + /** + * Clears this instance from listening to method calls. + * + *

Does nothing if {@link #startListening} hasn't been called, or if we're already stopped. + */ + void stopListening() { + if (channel == null) { + Log.d(TAG, "Tried to stop listening when no MethodChannel had been initialized."); + return; + } + + channel.setMethodCallHandler(null); + channel = null; + } + + private void onCanLaunch(Result result, String url) { + result.success(urlLauncher.canLaunch(url)); + } + + private void onLaunch(MethodCall call, Result result, String url) { + final boolean useWebView = call.argument("useWebView"); + final boolean enableJavaScript = call.argument("enableJavaScript"); + final boolean enableDomStorage = call.argument("enableDomStorage"); + final Map headersMap = call.argument("headers"); + final Bundle headersBundle = extractBundle(headersMap); + + LaunchStatus launchStatus = + urlLauncher.launch(url, headersBundle, useWebView, enableJavaScript, enableDomStorage); + + if (launchStatus == LaunchStatus.NO_ACTIVITY) { + result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); + } else if (launchStatus == LaunchStatus.ACTIVITY_NOT_FOUND) { + result.error( + "ACTIVITY_NOT_FOUND", + String.format("No Activity found to handle intent { %s }", url), + null); + } else { + result.success(true); + } + } + + private void onCloseWebView(Result result) { + urlLauncher.closeWebView(); + result.success(null); + } + + private static Bundle extractBundle(Map headersMap) { + final Bundle headersBundle = new Bundle(); + for (String key : headersMap.keySet()) { + final String value = headersMap.get(key); + headersBundle.putString(key, value); + } + return headersBundle; + } +} diff --git a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java similarity index 88% rename from packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java rename to packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java index 07f7ef3ee7dc..c3a563a9c137 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java @@ -12,11 +12,14 @@ import android.net.Uri; import android.os.Bundle; import android.provider.Browser; +import android.util.Log; import androidx.annotation.Nullable; /** Launches components for URLs. */ class UrlLauncher { + private static final String TAG = "UrlLauncher"; private final Context applicationContext; + @Nullable private Activity activity; /** @@ -40,9 +43,14 @@ boolean canLaunch(String url) { ComponentName componentName = launchIntent.resolveActivity(applicationContext.getPackageManager()); - return componentName != null - && !"{com.android.fallback/com.android.fallback.Fallback}" - .equals(componentName.toShortString()); + if (componentName == null) { + Log.i(TAG, "component name for " + url + " is null"); + return false; + } else { + Log.i(TAG, "component name for " + url + " is " + componentName.toShortString()); + return !"{com.android.fallback/com.android.fallback.Fallback}" + .equals(componentName.toShortString()); + } } /** diff --git a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java similarity index 100% rename from packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java rename to packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java diff --git a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java similarity index 95% rename from packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java rename to packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java index 7f26c18740ec..ec8cde514621 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java @@ -20,7 +20,10 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -142,7 +145,11 @@ public void onCreate(Bundle savedInstanceState) { registerReceiver(broadcastReceiver, closeIntentFilter); } - private Map extractHeaders(Bundle headersBundle) { + @VisibleForTesting + public static Map extractHeaders(@Nullable Bundle headersBundle) { + if (headersBundle == null) { + return Collections.emptyMap(); + } final Map headersMap = new HashMap<>(); for (String key : headersBundle.keySet()) { final String value = headersBundle.getString(key); 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 new file mode 100644 index 000000000000..b60192531dbd --- /dev/null +++ b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/MethodCallHandlerImplTest.java @@ -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. + +package io.flutter.plugins.urllauncher; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Bundle; +import androidx.test.core.app.ApplicationProvider; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.BinaryMessenger.BinaryMessageHandler; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel.Result; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class MethodCallHandlerImplTest { + private static final String CHANNEL_NAME = "plugins.flutter.io/url_launcher_android"; + private UrlLauncher urlLauncher; + private MethodCallHandlerImpl methodCallHandler; + + @Before + public void setUp() { + urlLauncher = new UrlLauncher(ApplicationProvider.getApplicationContext(), /*activity=*/ null); + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + } + + @Test + public void startListening_registersChannel() { + BinaryMessenger messenger = mock(BinaryMessenger.class); + + methodCallHandler.startListening(messenger); + + verify(messenger, times(1)) + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + } + + @Test + public void startListening_unregistersExistingChannel() { + BinaryMessenger firstMessenger = mock(BinaryMessenger.class); + BinaryMessenger secondMessenger = mock(BinaryMessenger.class); + methodCallHandler.startListening(firstMessenger); + + methodCallHandler.startListening(secondMessenger); + + // Unregisters the first and then registers the second. + verify(firstMessenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + verify(secondMessenger, times(1)) + .setMessageHandler(eq(CHANNEL_NAME), any(BinaryMessageHandler.class)); + } + + @Test + public void stopListening_unregistersExistingChannel() { + BinaryMessenger messenger = mock(BinaryMessenger.class); + methodCallHandler.startListening(messenger); + + methodCallHandler.stopListening(); + + verify(messenger, times(1)).setMessageHandler(CHANNEL_NAME, null); + } + + @Test + public void stopListening_doesNothingWhenUnset() { + BinaryMessenger messenger = mock(BinaryMessenger.class); + + methodCallHandler.stopListening(); + + verify(messenger, never()).setMessageHandler(CHANNEL_NAME, null); + } + + @Test + public void onMethodCall_canLaunchReturnsTrue() { + urlLauncher = mock(UrlLauncher.class); + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + String url = "foo"; + when(urlLauncher.canLaunch(url)).thenReturn(true); + Result result = mock(Result.class); + Map args = new HashMap<>(); + args.put("url", url); + + methodCallHandler.onMethodCall(new MethodCall("canLaunch", args), result); + + verify(result, times(1)).success(true); + } + + @Test + public void onMethodCall_canLaunchReturnsFalse() { + urlLauncher = mock(UrlLauncher.class); + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + String url = "foo"; + when(urlLauncher.canLaunch(url)).thenReturn(false); + Result result = mock(Result.class); + Map args = new HashMap<>(); + args.put("url", url); + + methodCallHandler.onMethodCall(new MethodCall("canLaunch", args), result); + + verify(result, times(1)).success(false); + } + + @Test + public void onMethodCall_launchReturnsNoActivityError() { + // Setup mock objects + urlLauncher = mock(UrlLauncher.class); + Result result = mock(Result.class); + // Setup expected values + String url = "foo"; + boolean useWebView = false; + boolean enableJavaScript = false; + boolean enableDomStorage = false; + // Setup arguments map send on the method channel + Map args = new HashMap<>(); + args.put("url", url); + args.put("useWebView", useWebView); + args.put("enableJavaScript", enableJavaScript); + args.put("enableDomStorage", enableDomStorage); + args.put("headers", new HashMap<>()); + // Mock the launch method on the urlLauncher class + when(urlLauncher.launch( + eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage))) + .thenReturn(UrlLauncher.LaunchStatus.NO_ACTIVITY); + // Act by calling the "launch" method on the method channel + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + // Verify the results and assert + verify(result, times(1)) + .error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); + } + + @Test + public void onMethodCall_launchReturnsActivityNotFoundError() { + // Setup mock objects + urlLauncher = mock(UrlLauncher.class); + Result result = mock(Result.class); + // Setup expected values + String url = "foo"; + boolean useWebView = false; + boolean enableJavaScript = false; + boolean enableDomStorage = false; + // Setup arguments map send on the method channel + Map args = new HashMap<>(); + args.put("url", url); + args.put("useWebView", useWebView); + args.put("enableJavaScript", enableJavaScript); + args.put("enableDomStorage", enableDomStorage); + args.put("headers", new HashMap<>()); + // Mock the launch method on the urlLauncher class + when(urlLauncher.launch( + eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage))) + .thenReturn(UrlLauncher.LaunchStatus.ACTIVITY_NOT_FOUND); + // Act by calling the "launch" method on the method channel + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + // Verify the results and assert + verify(result, times(1)) + .error( + "ACTIVITY_NOT_FOUND", + String.format("No Activity found to handle intent { %s }", url), + null); + } + + @Test + public void onMethodCall_launchReturnsTrue() { + // Setup mock objects + urlLauncher = mock(UrlLauncher.class); + Result result = mock(Result.class); + // Setup expected values + String url = "foo"; + boolean useWebView = false; + boolean enableJavaScript = false; + boolean enableDomStorage = false; + // Setup arguments map send on the method channel + Map args = new HashMap<>(); + args.put("url", url); + args.put("useWebView", useWebView); + args.put("enableJavaScript", enableJavaScript); + args.put("enableDomStorage", enableDomStorage); + args.put("headers", new HashMap<>()); + // Mock the launch method on the urlLauncher class + when(urlLauncher.launch( + eq(url), any(Bundle.class), eq(useWebView), eq(enableJavaScript), eq(enableDomStorage))) + .thenReturn(UrlLauncher.LaunchStatus.OK); + // Act by calling the "launch" method on the method channel + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + methodCallHandler.onMethodCall(new MethodCall("launch", args), result); + // Verify the results and assert + verify(result, times(1)).success(true); + } + + @Test + public void onMethodCall_closeWebView() { + urlLauncher = mock(UrlLauncher.class); + methodCallHandler = new MethodCallHandlerImpl(urlLauncher); + String url = "foo"; + when(urlLauncher.canLaunch(url)).thenReturn(true); + Result result = mock(Result.class); + Map args = new HashMap<>(); + args.put("url", url); + + methodCallHandler.onMethodCall(new MethodCall("closeWebView", args), result); + + verify(urlLauncher, times(1)).closeWebView(); + verify(result, times(1)).success(null); + } +} diff --git a/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.java b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.java new file mode 100644 index 000000000000..d0b0508d2307 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/android/src/test/java/io/flutter/plugins/urllauncher/WebViewActivityTest.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.urllauncher; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import org.junit.Test; + +public class WebViewActivityTest { + @Test + public void extractHeaders_returnsEmptyMapWhenHeadersBundleNull() { + assertEquals(WebViewActivity.extractHeaders(null), Collections.emptyMap()); + } +} diff --git a/packages/url_launcher/url_launcher_android/example/README.md b/packages/url_launcher/url_launcher_android/example/README.md new file mode 100644 index 000000000000..35b4bdb7031e --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/README.md @@ -0,0 +1,3 @@ +# url_launcher_example + +Demonstrates how to use the url_launcher plugin. 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 new file mode 100644 index 000000000000..8c7e84563ee6 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/android/app/build.gradle @@ -0,0 +1,60 @@ +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.urllauncherexample" + minSdkVersion 16 + 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' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/packages/package_info/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/package_info/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/url_launcher/url_launcher_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/url_launcher/url_launcher_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_android/example/android/app/src/androidTest/java/io/flutter/plugins/urllauncherexample/FlutterActivityTest.java b/packages/url_launcher/url_launcher_android/example/android/app/src/androidTest/java/io/flutter/plugins/urllauncherexample/FlutterActivityTest.java new file mode 100644 index 000000000000..67f15efb10aa --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/android/app/src/androidTest/java/io/flutter/plugins/urllauncherexample/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.urllauncherexample; + +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/url_launcher/url_launcher_android/example/android/app/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..918c29ee2dca --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/share/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/share/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/share/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/share/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/share/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/share/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/share/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/share/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/share/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/share/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/url_launcher/url_launcher_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/url_launcher/url_launcher_android/example/android/build.gradle b/packages/url_launcher/url_launcher_android/example/android/build.gradle new file mode 100644 index 000000000000..328175bb6ac5 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.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/connectivity/connectivity/example/android/gradle.properties b/packages/url_launcher/url_launcher_android/example/android/gradle.properties similarity index 100% rename from packages/connectivity/connectivity/example/android/gradle.properties rename to packages/url_launcher/url_launcher_android/example/android/gradle.properties 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 new file mode 100644 index 000000000000..4ae10e927b38 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Jul 31 20:16:04 BRT 2019 +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 diff --git a/packages/sensors/example/android/settings.gradle b/packages/url_launcher/url_launcher_android/example/android/settings.gradle similarity index 100% rename from packages/sensors/example/android/settings.gradle rename to packages/url_launcher/url_launcher_android/example/android/settings.gradle diff --git a/packages/url_launcher/url_launcher_android/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_android/example/integration_test/url_launcher_test.dart new file mode 100644 index 000000000000..28dc79b7af38 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/integration_test/url_launcher_test.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canLaunch', (WidgetTester _) async { + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); + + // sms:, tel:, and mailto: links may not be openable on every device, so + // aren't tested here. + }); +} diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart new file mode 100644 index 000000000000..672ae4a27665 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -0,0 +1,219 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'URL Launcher', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'URL Launcher'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + 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 { + if (!await launcher.launch( + url, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + )) { + throw 'Could not launch $url'; + } + } + + 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 { + 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 { + if (!await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + )) { + throw 'Could not launch $url'; + } + } + + Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const Text(''); + } + } + + 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( + title: Text(widget.title), + ), + body: ListView( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + onChanged: (String text) => _phone = text, + decoration: const InputDecoration( + hintText: 'Input the phone number to launch')), + ), + ElevatedButton( + 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), + child: Text(toLaunch), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebView(toLaunch); + }), + child: const Text('Launch in app'), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithJavaScript(toLaunch); + }), + child: const Text('Launch in app (JavaScript ON)'), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithDomStorage(toLaunch); + }), + child: const Text('Launch in app (DOM storage ON)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebView(toLaunch); + Timer(const Duration(seconds: 5), () { + print('Closing WebView after 5 seconds...'); + launcher.closeWebView(); + }); + }), + child: const Text('Launch in app + close after 5 seconds'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + FutureBuilder(future: _launched, builder: _launchStatus), + ], + ), + ], + ), + ); + } +} diff --git a/packages/url_launcher/url_launcher_android/example/pubspec.yaml b/packages/url_launcher/url_launcher_android/example/pubspec.yaml new file mode 100644 index 000000000000..cdb19458ba07 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: url_launcher_example +description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + url_launcher_android: + # When depending on this package from a real application you should use: + # url_launcher_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: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + mockito: ^5.0.0 + plugin_platform_interface: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/packages/url_launcher/url_launcher_android/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart new file mode 100644 index 000000000000..1aa093a36451 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.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/services.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/url_launcher_android'); + +/// An implementation of [UrlLauncherPlatform] for Android. +class UrlLauncherAndroid extends UrlLauncherPlatform { + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void registerWith() { + UrlLauncherPlatform.instance = UrlLauncherAndroid(); + } + + @override + final LinkDelegate? linkDelegate = null; + + @override + 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}, + ).then((bool? value) => value ?? false); + } + + @override + Future closeWebView() { + return _channel.invokeMethod('closeWebView'); + } + + @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, + }) { + return _channel.invokeMethod( + 'launch', + { + 'url': url, + 'useWebView': useWebView, + 'enableJavaScript': enableJavaScript, + 'enableDomStorage': enableDomStorage, + 'universalLinksOnly': universalLinksOnly, + 'headers': headers, + }, + ).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 new file mode 100644 index 000000000000..3c80170f1422 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -0,0 +1,30 @@ +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.17 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: url_launcher + platforms: + android: + package: io.flutter.plugins.urllauncher + pluginClass: UrlLauncherPlugin + dartPluginClass: UrlLauncherAndroid + +dependencies: + flutter: + sdk: flutter + url_launcher_platform_interface: ^2.0.3 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + plugin_platform_interface: ^2.0.0 + test: ^1.16.3 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 new file mode 100644 index 000000000000..eebd8cd4c059 --- /dev/null +++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart @@ -0,0 +1,288 @@ +// Copyright 2013 The Flutter Authors. 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:url_launcher_android/url_launcher_android.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_android'); + late List log; + + setUp(() { + log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + }); + + test('registers instance', () { + UrlLauncherAndroid.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); + + group('canLaunch', () { + test('calls through', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return true; + }); + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); + expect( + log, + [ + isMethodCall('canLaunch', arguments: { + 'url': 'http://example.com/', + }) + ], + ); + expect(canLaunch, true); + }); + + 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('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/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useWebView': false, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('passes headers', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {'key': 'value'}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useWebView': false, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {'key': 'value'}, + }) + ], + ); + }); + + test('handles universal links only', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + await launcher.launch( + 'http://example.com/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useWebView': false, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': true, + 'headers': {}, + }) + ], + ); + }); + + test('handles force WebView', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useWebView': true, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('handles force WebView with javascript', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useWebView': true, + 'enableJavaScript': true, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('handles force WebView with DOM storage', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useWebView': true, + 'enableJavaScript': false, + 'enableDomStorage': true, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('returns false if platform returns null', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + final bool launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + }); + + group('closeWebView', () { + test('calls through', () async { + final UrlLauncherAndroid launcher = UrlLauncherAndroid(); + await launcher.closeWebView(); + expect( + log, + [isMethodCall('closeWebView', arguments: null)], + ); + }); + }); +} diff --git a/packages/package_info/AUTHORS b/packages/url_launcher/url_launcher_ios/AUTHORS similarity index 100% rename from packages/package_info/AUTHORS rename to packages/url_launcher/url_launcher_ios/AUTHORS diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md new file mode 100644 index 000000000000..5fc00bff486e --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -0,0 +1,21 @@ +## 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. + +## 6.0.14 + +* Updates code for new analysis options. +* Removes dependency on `meta`. + +## 6.0.13 + +* Splits from `url_launcher` as a federated implementation. diff --git a/packages/url_launcher/url_launcher_ios/LICENSE b/packages/url_launcher/url_launcher_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/README.md b/packages/url_launcher/url_launcher_ios/README.md new file mode 100644 index 000000000000..56beaff77d6f --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/README.md @@ -0,0 +1,11 @@ +# url\_launcher\_ios + +The iOS implementation of [`url_launcher`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `url_launcher` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_ios/example/README.md b/packages/url_launcher/url_launcher_ios/example/README.md new file mode 100644 index 000000000000..35b4bdb7031e --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/README.md @@ -0,0 +1,3 @@ +# url_launcher_example + +Demonstrates how to use the url_launcher plugin. diff --git a/packages/url_launcher/url_launcher_ios/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_ios/example/integration_test/url_launcher_test.dart new file mode 100644 index 000000000000..b8f19053f709 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/integration_test/url_launcher_test.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canLaunch', (WidgetTester _) async { + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); + + // SMS handling is available by default on test devices. + expect(await launcher.canLaunch('sms:5555555555'), true); + + // tel: and mailto: links may not be openable on every device. iOS + // simulators notably can't open these link types. + }); +} diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Flutter/Debug.xcconfig b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..9803018ca79d --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Flutter/Release.xcconfig b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..a4a8c604e13d --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Podfile b/packages/url_launcher/url_launcher_ios/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Podfile @@ -0,0 +1,41 @@ +# 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 + 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/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..595f85d9a75b --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,718 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; + 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 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 */; }; + B8140773523F70A044426500 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */; }; + F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */; }; + F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F5826604D060028CB91 /* URLLauncherUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F7151F4D26604CFB0028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F5B26604D060028CB91 /* 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 */ + 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; + 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 666BCD7C181C34F8BE58929B /* 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 856D0913184F79C678A42603 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + D25C434271ACF6555E002440 /* 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 = ""; }; + F7151F4826604CFB0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherTests.m; sourceTree = ""; }; + F7151F4C26604CFB0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7151F5626604D060028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F5826604D060028CB91 /* URLLauncherUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherUITests.m; sourceTree = ""; }; + F7151F5A26604D060028CB91 /* 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 = ( + 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F4526604CFB0028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B8140773523F70A044426500 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F5326604D060028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + isa = PBXGroup; + children = ( + 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */, + A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */, + 666BCD7C181C34F8BE58929B /* Pods-RunnerTests.debug.xcconfig */, + D25C434271ACF6555E002440 /* 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 = ( + 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */, + 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F7151F4926604CFB0028CB91 /* RunnerTests */, + F7151F5726604D060028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + 840012C8B5EDBCF56B0E4AC1 /* Pods */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F4826604CFB0028CB91 /* RunnerTests.xctest */, + F7151F5626604D060028CB91 /* 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 */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 856D0913184F79C678A42603 /* libPods-Runner.a */, + 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + F7151F4926604CFB0028CB91 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */, + F7151F4C26604CFB0028CB91 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + F7151F5726604D060028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F5826604D060028CB91 /* URLLauncherUITests.m */, + F7151F5A26604D060028CB91 /* 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 */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F7151F4726604CFB0028CB91 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F5126604CFB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + DD4687403C4F35FCD2994FDE /* [CP] Check Pods Manifest.lock */, + F7151F4426604CFB0028CB91 /* Sources */, + F7151F4526604CFB0028CB91 /* Frameworks */, + F7151F4626604CFB0028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F4E26604CFB0028CB91 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F7151F4826604CFB0028CB91 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F7151F5526604D060028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F5D26604D060028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F5226604D060028CB91 /* Sources */, + F7151F5326604D060028CB91 /* Frameworks */, + F7151F5426604D060028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F5C26604D060028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F5626604D060028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = S8QB4VV633; + }; + F7151F4726604CFB0028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + F7151F5526604D060028CB91 = { + 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 */, + F7151F4726604CFB0028CB91 /* RunnerTests */, + F7151F5526604D060028CB91 /* 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; + }; + F7151F4626604CFB0028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F5426604D060028CB91 /* 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"; + }; + 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; + }; + DD4687403C4F35FCD2994FDE /* [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 */, + 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F4426604CFB0028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F5226604D060028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F7151F4E26604CFB0028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F4D26604CFB0028CB91 /* PBXContainerItemProxy */; + }; + F7151F5C26604D060028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F5B26604D060028CB91 /* 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.urlLauncher; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.urlLauncher; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F7151F4F26604CFB0028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 666BCD7C181C34F8BE58929B /* 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"; + 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; + }; + F7151F5026604CFB0028CB91 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D25C434271ACF6555E002440 /* 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"; + 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; + }; + F7151F5E26604D060028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + 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; + }; + F7151F5F26604D060028CB91 /* Release */ = { + isa = XCBuildConfiguration; + 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; + }; + F7151F5126604CFB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F4F26604CFB0028CB91 /* Debug */, + F7151F5026604CFB0028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F5D26604D060028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F5E26604D060028CB91 /* Debug */, + F7151F5F26604D060028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c5f1a9de4a30 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/url_launcher/url_launcher_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/AppDelegate.h b/packages/url_launcher/url_launcher_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/AppDelegate.m b/packages/url_launcher/url_launcher_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..83f0621aceba --- /dev/null +++ b/packages/url_launcher/url_launcher_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]; + [super application:application didFinishLaunchingWithOptions:launchOptions]; + return YES; +} + +@end diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..ebf48f603974 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..80aec052fa79 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + url_launcher_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/main.m b/packages/url_launcher/url_launcher_ios/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/video_player/video_player/example/ios/RunnerUITests/Info.plist b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/video_player/video_player/example/ios/RunnerUITests/Info.plist rename to packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/Info.plist diff --git a/packages/url_launcher/url_launcher/example/ios/RunnerTests/URLLauncherTests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m similarity index 78% rename from packages/url_launcher/url_launcher/example/ios/RunnerTests/URLLauncherTests.m rename to packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m index 746089425f7a..6507a95a9d07 100644 --- a/packages/url_launcher/url_launcher/example/ios/RunnerTests/URLLauncherTests.m +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.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 url_launcher; +@import url_launcher_ios; @import XCTest; @interface URLLauncherTests : XCTestCase @@ -11,7 +11,7 @@ @interface URLLauncherTests : XCTestCase @implementation URLLauncherTests - (void)testPlugin { - FLTURLLauncherPlugin* plugin = [[FLTURLLauncherPlugin alloc] init]; + FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init]; XCTAssertNotNil(plugin); } diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/Info.plist b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher/example/ios/RunnerUITests/URLLauncherUITests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m similarity index 78% rename from packages/url_launcher/url_launcher/example/ios/RunnerUITests/URLLauncherUITests.m rename to packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m index 18af3be9a1e5..b6d3bceff039 100644 --- a/packages/url_launcher/url_launcher/example/ios/RunnerUITests/URLLauncherUITests.m +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m @@ -6,7 +6,7 @@ @import os.log; @interface URLLauncherUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @implementation URLLauncherUITests @@ -19,18 +19,18 @@ - (void)setUp { } - (void)testLaunch { - XCUIApplication* app = self.app; + XCUIApplication *app = self.app; - NSArray* buttonNames = @[ + NSArray *buttonNames = @[ @"Launch in app", @"Launch in app(JavaScript ON)", @"Launch in app(DOM storage ON)", @"Launch a universal link in a native app, fallback to Safari.(Youtube)" ]; - for (NSString* buttonName in buttonNames) { - XCUIElement* button = app.buttons[buttonName]; + for (NSString *buttonName in buttonNames) { + XCUIElement *button = app.buttons[buttonName]; XCTAssertTrue([button waitForExistenceWithTimeout:30.0]); XCTAssertEqual(app.webViews.count, 0); [button tap]; - XCUIElement* webView = app.webViews.firstMatch; + XCUIElement *webView = app.webViews.firstMatch; XCTAssertTrue([webView waitForExistenceWithTimeout:30.0]); XCTAssertTrue([app.buttons[@"ForwardButton"] waitForExistenceWithTimeout:30.0]); XCTAssertTrue(app.buttons[@"Share"].exists); diff --git a/packages/url_launcher/url_launcher_ios/example/lib/main.dart b/packages/url_launcher/url_launcher_ios/example/lib/main.dart new file mode 100644 index 000000000000..7aa3a4b74e83 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/lib/main.dart @@ -0,0 +1,243 @@ +// Copyright 2013 The Flutter 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:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'URL Launcher', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'URL Launcher'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + Future? _launched; + String _phone = ''; + + 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 { + 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 { + 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 { + 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 { + throw 'Could not launch $url'; + } + } + + Future _launchUniversalLinkIos(String url) async { + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + if (await launcher.canLaunch(url)) { + final bool nativeAppLaunchSucceeded = await launcher.launch( + url, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: {}, + ); + if (!nativeAppLaunchSucceeded) { + await launcher.launch( + url, + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: {}, + ); + } + } + } + + Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const Text(''); + } + } + + 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'; + } + } + + @override + Widget build(BuildContext context) { + const String toLaunch = 'https://www.cylog.org/headers/'; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: ListView( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + onChanged: (String text) => _phone = text, + decoration: const InputDecoration( + hintText: 'Input the phone number to launch')), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _makePhoneCall('tel:$_phone'); + }), + child: const Text('Make phone call'), + ), + const Padding( + padding: EdgeInsets.all(16.0), + child: Text(toLaunch), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewOrVC(toLaunch); + }), + child: const Text('Launch in app'), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithJavaScript(toLaunch); + }), + child: const Text('Launch in app(JavaScript ON)'), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithDomStorage(toLaunch); + }), + child: const Text('Launch in app(DOM storage ON)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchUniversalLinkIos(toLaunch); + }), + child: const Text( + 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewOrVC(toLaunch); + Timer(const Duration(seconds: 5), () { + print('Closing WebView after 5 seconds...'); + UrlLauncherPlatform.instance.closeWebView(); + }); + }), + child: const Text('Launch in app + close after 5 seconds'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + FutureBuilder(future: _launched, builder: _launchStatus), + ], + ), + ], + ), + ); + } +} diff --git a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml new file mode 100644 index 000000000000..2e39e92d5638 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: url_launcher_example +description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + url_launcher_ios: + # When depending on this package from a real application you should use: + # url_launcher_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: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + mockito: ^5.0.0 + plugin_platform_interface: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/packages/url_launcher/url_launcher_ios/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_ios/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/ios/Assets/.gitkeep b/packages/url_launcher/url_launcher_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/local_auth/ios/Assets/.gitkeep rename to packages/url_launcher/url_launcher_ios/ios/Assets/.gitkeep diff --git a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h similarity index 100% rename from packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.h rename to packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h diff --git a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m similarity index 95% rename from packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m rename to packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m index 9ba9b1331728..af720c87b8b2 100644 --- a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m @@ -63,7 +63,7 @@ @implementation FLTURLLauncherPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/url_launcher" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/url_launcher_ios" binaryMessenger:registrar.messenger]; FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init]; [registrar addMethodCallDelegate:plugin channel:channel]; @@ -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 new file mode 100644 index 000000000000..1c0e81964252 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'url_launcher_ios' + s.version = '0.0.1' + s.summary = 'Flutter plugin for launching a URL.' + s.description = <<-DESC +A Flutter plugin for making the underlying platform (Android or iOS) launch a URL. + DESC + 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/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' + s.dependency 'Flutter' + + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart new file mode 100644 index 000000000000..84b811b25728 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/url_launcher_ios'); + +/// An implementation of [UrlLauncherPlatform] for iOS. +class UrlLauncherIOS extends UrlLauncherPlatform { + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void registerWith() { + UrlLauncherPlatform.instance = UrlLauncherIOS(); + } + + @override + final LinkDelegate? linkDelegate = null; + + @override + Future canLaunch(String url) { + return _channel.invokeMethod( + 'canLaunch', + {'url': url}, + ).then((bool? value) => value ?? false); + } + + @override + Future closeWebView() { + return _channel.invokeMethod('closeWebView'); + } + + @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, + }) { + return _channel.invokeMethod( + 'launch', + { + 'url': url, + 'useSafariVC': useSafariVC, + 'enableJavaScript': enableJavaScript, + 'enableDomStorage': enableDomStorage, + 'universalLinksOnly': universalLinksOnly, + 'headers': headers, + }, + ).then((bool? value) => value ?? false); + } +} diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml new file mode 100644 index 000000000000..9bb1616441b3 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -0,0 +1,29 @@ +name: url_launcher_ios +description: iOS implementation of the url_launcher plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 +version: 6.0.17 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: url_launcher + platforms: + ios: + pluginClass: FLTURLLauncherPlugin + dartPluginClass: UrlLauncherIOS + +dependencies: + flutter: + sdk: flutter + url_launcher_platform_interface: ^2.0.3 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + plugin_platform_interface: ^2.0.0 + test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart new file mode 100644 index 000000000000..8fad5807bddb --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -0,0 +1,208 @@ +// Copyright 2013 The Flutter Authors. 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:url_launcher_ios/url_launcher_ios.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$UrlLauncherIOS', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_ios'); + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('registers instance', () { + UrlLauncherIOS.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); + + test('canLaunch', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.canLaunch('http://example.com/'); + expect( + log, + [ + isMethodCall('canLaunch', arguments: { + 'url': 'http://example.com/', + }) + ], + ); + }); + + test('canLaunch should return false if platform returns null', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); + + expect(canLaunch, false); + }); + + test('launch', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': true, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch with headers', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {'key': 'value'}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': true, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {'key': 'value'}, + }) + ], + ); + }); + + test('launch force SafariVC', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': true, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch universal links only', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': false, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': true, + 'headers': {}, + }) + ], + ); + }); + + test('launch force SafariVC to false', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': false, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch should return false if platform returns null', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + final bool launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + + test('closeWebView default behavior', () async { + final UrlLauncherIOS launcher = UrlLauncherIOS(); + await launcher.closeWebView(); + expect( + log, + [isMethodCall('closeWebView', arguments: null)], + ); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index 147d0f312c7e..27c18a66805b 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,25 @@ +## 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 + existing versions of `url_launcher`, requiring Dart registration in this + package is in practice a breaking change. + * Does not include any API changes; clients can allow both 2.x or 3.x. + +## 2.0.4 + +* **\[Retracted\]** Switches to an in-package method channel implementation. + +## 2.0.3 + +* Updates code for new analysis options. +* Fix minor memory leak in Linux url_launcher tests. +* Fixes canLaunch detection for URIs addressing on local or network file systems + ## 2.0.2 * Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. diff --git a/packages/url_launcher/url_launcher_linux/example/README.md b/packages/url_launcher/url_launcher_linux/example/README.md index c200da8974d1..35b4bdb7031e 100644 --- a/packages/url_launcher/url_launcher_linux/example/README.md +++ b/packages/url_launcher/url_launcher_linux/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_linux/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart index ae9a9148f9d7..c9d0d8c9c096 100644 --- a/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart @@ -10,7 +10,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('canLaunch', (WidgetTester _) async { - UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; expect(await launcher.canLaunch('randomstring'), false); 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 86e06f3fafed..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( @@ -20,17 +22,17 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'URL Launcher'), + home: const MyHomePage(title: 'URL Launcher'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({Key? key, required this.title}) : super(key: key); 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/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt index 1758aac03b0d..11219aa55928 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt @@ -45,6 +45,8 @@ add_dependencies(${BINARY_NAME} flutter_assemble) # Enable the test target. set(include_url_launcher_linux_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS url_launcher_linux_test) # Generated plugin build rules, which manage building the plugins and adding # them to the application. diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index f6f23bfe970f..000000000000 --- a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc08f..000000000000 --- a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index b0ef2e6eddbf..90ea19dd2a04 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.8.0" dependencies: flutter: @@ -19,11 +19,10 @@ dependencies: url_launcher_platform_interface: ^2.0.0 dev_dependencies: - integration_test: - sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.10.0 + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart new file mode 100644 index 000000000000..87ef3142e3f6 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/url_launcher_linux'); + +/// An implementation of [UrlLauncherPlatform] for Linux. +class UrlLauncherLinux extends UrlLauncherPlatform { + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void registerWith() { + UrlLauncherPlatform.instance = UrlLauncherLinux(); + } + + @override + final LinkDelegate? linkDelegate = null; + + @override + Future canLaunch(String url) { + return _channel.invokeMethod( + 'canLaunch', + {'url': url}, + ).then((bool? value) => value ?? false); + } + + @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, + }) { + return _channel.invokeMethod( + 'launch', + { + 'url': url, + 'enableJavaScript': enableJavaScript, + 'enableDomStorage': enableDomStorage, + 'universalLinksOnly': universalLinksOnly, + 'headers': headers, + }, + ).then((bool? value) => value ?? false); + } +} diff --git a/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc b/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc index e655638c4ed7..2aa37aabb0b1 100644 --- a/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc +++ b/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc @@ -18,7 +18,7 @@ TEST(UrlLauncherPlugin, CanLaunchSuccess) { g_autoptr(FlValue) args = fl_value_new_map(); fl_value_set_string_take(args, "url", fl_value_new_string("https://flutter.dev")); - FlMethodResponse* response = can_launch(nullptr, args); + g_autoptr(FlMethodResponse) response = can_launch(nullptr, args); ASSERT_NE(response, nullptr); ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); g_autoptr(FlValue) expected = fl_value_new_bool(true); @@ -30,7 +30,32 @@ TEST(UrlLauncherPlugin, CanLaunchSuccess) { TEST(UrlLauncherPlugin, CanLaunchFailureUnhandled) { g_autoptr(FlValue) args = fl_value_new_map(); fl_value_set_string_take(args, "url", fl_value_new_string("madeup:scheme")); - FlMethodResponse* response = can_launch(nullptr, args); + g_autoptr(FlMethodResponse) response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(false); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +TEST(UrlLauncherPlugin, CanLaunchFileSuccess) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", fl_value_new_string("file:///")); + g_autoptr(FlMethodResponse) response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(true); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +TEST(UrlLauncherPlugin, CanLaunchFailureInvalidFileExtension) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take( + args, "url", fl_value_new_string("file:///madeup.madeupextension")); + g_autoptr(FlMethodResponse) response = can_launch(nullptr, args); ASSERT_NE(response, nullptr); ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); g_autoptr(FlValue) expected = fl_value_new_bool(false); @@ -44,7 +69,7 @@ TEST(UrlLauncherPlugin, CanLaunchFailureUnhandled) { TEST(UrlLauncherPlugin, CanLaunchFailureInvalidUrl) { g_autoptr(FlValue) args = fl_value_new_map(); fl_value_set_string_take(args, "url", fl_value_new_string("")); - FlMethodResponse* response = can_launch(nullptr, args); + g_autoptr(FlMethodResponse) response = can_launch(nullptr, args); ASSERT_NE(response, nullptr); ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); g_autoptr(FlValue) expected = fl_value_new_bool(false); diff --git a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc index d3f454ee7198..b0c7fece0e7c 100644 --- a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc +++ b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc @@ -12,7 +12,7 @@ #include "url_launcher_plugin_private.h" // See url_launcher_channel.dart for documentation. -const char kChannelName[] = "plugins.flutter.io/url_launcher"; +const char kChannelName[] = "plugins.flutter.io/url_launcher_linux"; const char kBadArgumentsError[] = "Bad Arguments"; const char kLaunchError[] = "Launch Error"; const char kCanLaunchMethod[] = "canLaunch"; @@ -45,6 +45,16 @@ static gchar* get_url(FlValue* args, GError** error) { return g_strdup(fl_value_get_string(url_value)); } +// Checks if URI has launchable file resource. +static gboolean can_launch_uri_with_file_resource(FlUrlLauncherPlugin* self, + const gchar* url) { + g_autoptr(GError) error = nullptr; + g_autoptr(GFile) file = g_file_new_for_uri(url); + g_autoptr(GAppInfo) app_info = + g_file_query_default_handler(file, NULL, &error); + return app_info != nullptr; +} + // Called to check if a URL can be launched. FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args) { g_autoptr(GError) error = nullptr; @@ -60,6 +70,10 @@ FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args) { g_autoptr(GAppInfo) app_info = g_app_info_get_default_for_uri_scheme(scheme); is_launchable = app_info != nullptr; + + if (!is_launchable) { + is_launchable = can_launch_uri_with_file_resource(self, url); + } } g_autoptr(FlValue) result = fl_value_new_bool(is_launchable); diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 960216851e5d..0bbd4b590cd2 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -1,12 +1,12 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux +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: 2.0.2 +version: 3.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,7 +14,14 @@ flutter: platforms: linux: pluginClass: UrlLauncherPlugin + dartPluginClass: UrlLauncherLinux dependencies: flutter: sdk: flutter + url_launcher_platform_interface: ^2.0.3 + +dev_dependencies: + flutter_test: + sdk: flutter + test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart new file mode 100644 index 000000000000..7a4399dd4e6c --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.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:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_linux/url_launcher_linux.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$UrlLauncherLinux', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_linux'); + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('registers instance', () { + UrlLauncherLinux.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); + + test('canLaunch', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + await launcher.canLaunch('http://example.com/'); + expect( + log, + [ + isMethodCall('canLaunch', arguments: { + 'url': 'http://example.com/', + }) + ], + ); + }); + + test('canLaunch should return false if platform returns null', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); + + expect(canLaunch, false); + }); + + test('launch', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch with headers', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {'key': 'value'}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {'key': 'value'}, + }) + ], + ); + }); + + test('launch universal links only', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + await launcher.launch( + 'http://example.com/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': true, + 'headers': {}, + }) + ], + ); + }); + + test('launch should return false if platform returns null', () async { + final UrlLauncherLinux launcher = UrlLauncherLinux(); + final bool launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 96d2fd49c7e7..2fa5e918eadd 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,24 @@ +## 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 + existing versions of `url_launcher`, requiring Dart registration in this + package is in practice a breaking change. + * Does not include any API changes; clients can allow both 2.x or 3.x. + +## 2.0.4 + +* **\[Retracted\]** Switches to an in-package method channel implementation. + +## 2.0.3 + +* Updates code for new analysis options. +* Updates unit tests. + ## 2.0.2 * Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. diff --git a/packages/url_launcher/url_launcher_macos/example/README.md b/packages/url_launcher/url_launcher_macos/example/README.md index c200da8974d1..35b4bdb7031e 100644 --- a/packages/url_launcher/url_launcher_macos/example/README.md +++ b/packages/url_launcher/url_launcher_macos/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_macos/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart index 897b22f89392..87bc3d21df07 100644 --- a/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart @@ -10,7 +10,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('canLaunch', (WidgetTester _) async { - UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; expect(await launcher.canLaunch('randomstring'), false); 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 86e06f3fafed..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( @@ -20,17 +22,17 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'URL Launcher'), + home: const MyHomePage(title: 'URL Launcher'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override - _MyHomePageState createState() => _MyHomePageState(); + State createState() => _MyHomePageState(); } class _MyHomePageState extends State { diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 8236f5728c63..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import url_launcher_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) -} diff --git a/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift index d08f66464454..adbd1144c8b9 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift @@ -6,19 +6,149 @@ import FlutterMacOS import XCTest import url_launcher_macos +/// A stub to simulate the system Url handler. +class StubWorkspace: SystemURLHandler { + + var isSuccessful = true + + func open(_ url: URL) -> Bool { + return isSuccessful + } + + func urlForApplication(toOpen: URL) -> URL? { + return toOpen + } +} + class RunnerTests: XCTestCase { - func testCanLaunch() throws { + + func testCanLaunchSuccessReturnsTrue() throws { + let expectation = XCTestExpectation(description: "Check if the URL can be launched") let plugin = UrlLauncherPlugin() + let call = FlutterMethodCall( methodName: "canLaunch", arguments: ["url": "https://flutter.dev"]) - var canLaunch: Bool? + + plugin.handle( + call, + result: { (result: Any?) -> Void in + XCTAssertEqual(result as? Bool, true) + expectation.fulfill() + }) + + wait(for: [expectation], timeout: 10.0) + } + + func testCanLaunchNoAppIsAbleToOpenUrlReturnsFalse() throws { + let expectation = XCTestExpectation(description: "Check if the URL can be launched") + let plugin = UrlLauncherPlugin() + + let call = FlutterMethodCall( + methodName: "canLaunch", + arguments: ["url": "example://flutter.dev"]) + + plugin.handle( + call, + result: { (result: Any?) -> Void in + XCTAssertEqual(result as? Bool, false) + expectation.fulfill() + }) + + wait(for: [expectation], timeout: 10.0) + } + + func testCanLaunchInvalidUrlReturnsFalse() throws { + let expectation = XCTestExpectation(description: "Check if the URL can be launched") + let plugin = UrlLauncherPlugin() + + let call = FlutterMethodCall( + methodName: "canLaunch", + arguments: ["url": "brokenUrl"]) + + plugin.handle( + call, + result: { (result: Any?) -> Void in + XCTAssertEqual(result as? Bool, false) + expectation.fulfill() + }) + + wait(for: [expectation], timeout: 10.0) + } + + func testCanLaunchMissingArgumentReturnsFlutterError() throws { + let expectation = XCTestExpectation(description: "Check if the URL can be launched") + let plugin = UrlLauncherPlugin() + + let call = FlutterMethodCall( + methodName: "canLaunch", + arguments: []) + plugin.handle( call, result: { (result: Any?) -> Void in - canLaunch = result as? Bool + XCTAssertTrue(result is FlutterError) + expectation.fulfill() + }) + + wait(for: [expectation], timeout: 10.0) + } + + func testLaunchSuccessReturnsTrue() throws { + let expectation = XCTestExpectation(description: "Try to open the URL") + let workspace = StubWorkspace() + let pluginWithStubWorkspace = UrlLauncherPlugin(workspace) + + let call = FlutterMethodCall( + methodName: "launch", + arguments: ["url": "https://flutter.dev"]) + + pluginWithStubWorkspace.handle( + call, + result: { (result: Any?) -> Void in + XCTAssertEqual(result as? Bool, true) + expectation.fulfill() + }) + + wait(for: [expectation], timeout: 10.0) + } + + func testLaunchNoAppIsAbleToOpenUrlReturnsFalse() throws { + let expectation = XCTestExpectation(description: "Try to open the URL") + let workspace = StubWorkspace() + workspace.isSuccessful = false + let pluginWithStubWorkspace = UrlLauncherPlugin(workspace) + + let call = FlutterMethodCall( + methodName: "launch", + arguments: ["url": "schemethatdoesnotexist://flutter.dev"]) + + pluginWithStubWorkspace.handle( + call, + result: { (result: Any?) -> Void in + XCTAssertEqual(result as? Bool, false) + expectation.fulfill() + }) + + wait(for: [expectation], timeout: 10.0) + } + + func testLaunchMissingArgumentReturnsFlutterError() throws { + let expectation = XCTestExpectation(description: "Try to open the URL") + let workspace = StubWorkspace() + let pluginWithStubWorkspace = UrlLauncherPlugin(workspace) + + let call = FlutterMethodCall( + methodName: "launch", + arguments: []) + + pluginWithStubWorkspace.handle( + call, + result: { (result: Any?) -> Void in + XCTAssertTrue(result is FlutterError) + expectation.fulfill() }) - XCTAssertTrue(canLaunch == true) + wait(for: [expectation], timeout: 10.0) } } diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index 6d12b75819b0..2652df03448a 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.8.0" dependencies: flutter: @@ -19,11 +19,10 @@ dependencies: url_launcher_platform_interface: ^2.0.0 dev_dependencies: - integration_test: - sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.10.0 + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart new file mode 100644 index 000000000000..7dc1340083ae --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/url_launcher_macos'); + +/// An implementation of [UrlLauncherPlatform] for macOS. +class UrlLauncherMacOS extends UrlLauncherPlatform { + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void registerWith() { + UrlLauncherPlatform.instance = UrlLauncherMacOS(); + } + + @override + final LinkDelegate? linkDelegate = null; + + @override + Future canLaunch(String url) { + return _channel.invokeMethod( + 'canLaunch', + {'url': url}, + ).then((bool? value) => value ?? false); + } + + @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, + }) { + return _channel.invokeMethod( + 'launch', + { + 'url': url, + 'enableJavaScript': enableJavaScript, + 'enableDomStorage': enableDomStorage, + 'universalLinksOnly': universalLinksOnly, + 'headers': headers, + }, + ).then((bool? value) => value ?? false); + } +} diff --git a/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift b/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift index ab89038fa01d..4b799ee12094 100644 --- a/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift @@ -5,10 +5,38 @@ import FlutterMacOS import Foundation +/// A handler that can launch other apps, check if any app is able to open the URL. +public protocol SystemURLHandler { + + /// Opens the location at the specified URL. + /// + /// - Parameters: + /// - url: A URL specifying the location to open. + /// - Returns: true if the location was successfully opened; otherwise, false. + func open(_ url: URL) -> Bool + + /// Returns the URL to the default app that would be opened. + /// + /// - Parameters: + /// - toOpen: The URL of the file to open. + /// - Returns: The URL of the default app that would open the specified url. + /// Returns nil if no app is able to open the URL, or if the file URL does not exist. + func urlForApplication(toOpen: URL) -> URL? +} + +extension NSWorkspace: SystemURLHandler {} + public class UrlLauncherPlugin: NSObject, FlutterPlugin { + + private var workspace: SystemURLHandler + + public init(_ workspace: SystemURLHandler = NSWorkspace.shared) { + self.workspace = workspace + } + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( - name: "plugins.flutter.io/url_launcher", + name: "plugins.flutter.io/url_launcher_macos", binaryMessenger: registrar.messenger) let instance = UrlLauncherPlugin() registrar.addMethodCallDelegate(instance, channel: channel) @@ -24,7 +52,7 @@ public class UrlLauncherPlugin: NSObject, FlutterPlugin { result(invalidURLError(urlString)) return } - result(NSWorkspace.shared.urlForApplication(toOpen: url) != nil) + result(workspace.urlForApplication(toOpen: url) != nil) case "launch": guard let unwrappedURLString = urlString, let url = URL.init(string: unwrappedURLString) @@ -32,7 +60,7 @@ public class UrlLauncherPlugin: NSObject, FlutterPlugin { result(invalidURLError(urlString)) return } - result(NSWorkspace.shared.open(url)) + result(workspace.open(url)) default: result(FlutterMethodNotImplemented) } 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 3db112b64be2..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 @@ -8,10 +8,10 @@ Pod::Spec.new do |s| s.description = <<-DESC A macOS implementation of the url_launcher plugin. DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos' + 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 534830000626..8f93e57c9dc4 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -1,12 +1,12 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos +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: 2.0.2 +version: 3.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -15,10 +15,14 @@ flutter: macos: pluginClass: UrlLauncherPlugin fileName: url_launcher_macos.dart + dartPluginClass: UrlLauncherMacOS dependencies: flutter: sdk: flutter + url_launcher_platform_interface: ^2.0.3 dev_dependencies: - pedantic: ^1.8.0 + flutter_test: + sdk: flutter + test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart new file mode 100644 index 000000000000..0a28aea678c3 --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.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:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_macos/url_launcher_macos.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$UrlLauncherMacOS', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_macos'); + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('registers instance', () { + UrlLauncherMacOS.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); + + test('canLaunch', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(); + await launcher.canLaunch('http://example.com/'); + expect( + log, + [ + isMethodCall('canLaunch', arguments: { + 'url': 'http://example.com/', + }) + ], + ); + }); + + test('canLaunch should return false if platform returns null', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); + + expect(canLaunch, false); + }); + + test('launch', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch with headers', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {'key': 'value'}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {'key': 'value'}, + }) + ], + ); + }); + + test('launch universal links only', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(); + await launcher.launch( + 'http://example.com/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': true, + 'headers': {}, + }) + ], + ); + }); + + test('launch should return false if platform returns null', () async { + final UrlLauncherMacOS launcher = UrlLauncherMacOS(); + final bool launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index fc56473533f2..78818eff7bbf 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.0 + +* Adds a new `launchUrl` method corresponding to the new app-facing interface. + +## 2.0.5 + +* Updates code for new analysis options. +* Update to use the `verify` method introduced in platform_plugin_interface 2.1.0. + ## 2.0.4 * Silenced warnings that may occur during build when using a very diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart index ffff14feb8d7..da8aa1570bad 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -22,7 +22,7 @@ typedef LinkWidgetBuilder = Widget Function( /// Signature for a delegate function to build the [Link] widget. typedef LinkDelegate = Widget Function(LinkInfo linkWidget); -final MethodCodec _codec = const JSONMethodCodec(); +const MethodCodec _codec = JSONMethodCodec(); /// Defines where a Link URL should be open. /// @@ -42,20 +42,21 @@ class LinkTarget { /// /// iOS, on the other hand, defaults to [self] for web URLs, and [blank] for /// non-web URLs. - static const defaultTarget = LinkTarget._(debugLabel: 'defaultTarget'); + static const LinkTarget defaultTarget = + LinkTarget._(debugLabel: 'defaultTarget'); /// On the web, this opens the link in the same tab where the flutter app is /// running. /// /// On Android and iOS, this opens the link in a webview within the app. - static const self = LinkTarget._(debugLabel: 'self'); + static const LinkTarget self = LinkTarget._(debugLabel: 'self'); /// On the web, this opens the link in a new tab or window (depending on the /// browser and user configuration). /// /// On Android and iOS, this opens the link in the browser or the relevant /// app. - static const blank = LinkTarget._(debugLabel: 'blank'); + static const LinkTarget blank = LinkTarget._(debugLabel: 'blank'); } /// Encapsulates all the information necessary to build a Link widget. diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart index e75e283eeca7..df738046b96b 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart @@ -21,7 +21,7 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { return _channel.invokeMethod( 'canLaunch', {'url': url}, - ).then((value) => value ?? false); + ).then((bool? value) => value ?? false); } @override @@ -51,6 +51,6 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { 'universalLinksOnly': universalLinksOnly, 'headers': headers, }, - ).then((value) => value ?? false); + ).then((bool? value) => value ?? false); } } 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..aa499db4ce6f --- /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 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.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.'); + } + + /// 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 e9435b8dc4e3..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.verifyToken(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 074e95b08c2c..76461ff7b979 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -1,22 +1,21 @@ name: url_launcher_platform_interface description: A common platform interface for the url_launcher plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_platform_interface 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.4 +version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.0 + plugin_platform_interface: ^2.1.0 dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart index 75a14b2e11a6..a6b316dacec3 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart @@ -13,20 +13,20 @@ import 'package:url_launcher_platform_interface/link.dart'; void main() { testWidgets('Link with Navigator', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( - home: Placeholder(key: Key('home')), + home: const Placeholder(key: Key('home')), routes: { - '/a': (BuildContext context) => Placeholder(key: Key('a')), + '/a': (BuildContext context) => const Placeholder(key: Key('a')), }, )); - expect(find.byKey(Key('home')), findsOneWidget); - expect(find.byKey(Key('a')), findsNothing); + expect(find.byKey(const Key('home')), findsOneWidget); + expect(find.byKey(const Key('a')), findsNothing); await tester.runAsync(() => pushRouteNameToFramework(null, '/a')); // start animation await tester.pump(); // skip past animation (5s is arbitrary, just needs to be long enough) await tester.pump(const Duration(seconds: 5)); - expect(find.byKey(Key('a')), findsOneWidget); - expect(find.byKey(Key('home')), findsNothing); + expect(find.byKey(const Key('a')), findsOneWidget); + expect(find.byKey(const Key('home')), findsNothing); }); testWidgets('Link with Navigator', (WidgetTester tester) async { @@ -34,15 +34,15 @@ void main() { routeInformationParser: _RouteInformationParser(), routerDelegate: _RouteDelegate(), )); - expect(find.byKey(Key('/')), findsOneWidget); - expect(find.byKey(Key('/a')), findsNothing); + expect(find.byKey(const Key('/')), findsOneWidget); + expect(find.byKey(const Key('/a')), findsNothing); await tester.runAsync(() => pushRouteNameToFramework(null, '/a')); // start animation await tester.pump(); // skip past animation (5s is arbitrary, just needs to be long enough) await tester.pump(const Duration(seconds: 5)); - expect(find.byKey(Key('/a')), findsOneWidget); - expect(find.byKey(Key('/')), findsNothing); + expect(find.byKey(const Key('/a')), findsOneWidget); + expect(find.byKey(const Key('/')), findsNothing); }); } @@ -50,7 +50,7 @@ class _RouteInformationParser extends RouteInformationParser { @override Future parseRouteInformation( RouteInformation routeInformation) { - return SynchronousFuture(routeInformation); + return SynchronousFuture(routeInformation); } @override @@ -66,22 +66,22 @@ class _RouteDelegate extends RouterDelegate @override Future setNewRoutePath(RouteInformation configuration) { _history.add(configuration); - return SynchronousFuture(null); + return SynchronousFuture(null); } @override Future popRoute() { if (_history.isEmpty) { - return SynchronousFuture(false); + return SynchronousFuture(false); } _history.removeLast(); - return SynchronousFuture(true); + return SynchronousFuture(true); } @override Widget build(BuildContext context) { if (_history.isEmpty) { - return Placeholder(key: Key('empty')); + return const Placeholder(key: Key('empty')); } return Placeholder(key: Key('${_history.last.location}')); } 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 5bfc78c5c5a2..e44e80bab02c 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart @@ -2,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:mockito/mockito.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/method_channel_url_launcher.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; @@ -14,10 +13,12 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final UrlLauncherPlatform initialInstance = UrlLauncherPlatform.instance; + group('$UrlLauncherPlatform', () { test('$MethodChannelUrlLauncher() is the default instance', () { - expect(UrlLauncherPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Cannot be implemented with `implements`', () { @@ -67,7 +68,7 @@ void main() { }); test('canLaunch should return false if platform returns null', () async { - final canLaunch = await launcher.canLaunch('http://example.com/'); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); expect(canLaunch, false); }); @@ -281,7 +282,7 @@ void main() { }); test('launch should return false if platform returns null', () async { - final launched = await launcher.launch( + final bool launched = await launcher.launch( 'http://example.com/', useSafariVC: true, useWebView: false, 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/AUTHORS b/packages/url_launcher/url_launcher_web/AUTHORS index 493a0b4ef9c2..2678aaba8101 100644 --- a/packages/url_launcher/url_launcher_web/AUTHORS +++ b/packages/url_launcher/url_launcher_web/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +TheOneWithTheBraid \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index f5338e62a775..75c0819cefdc 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,6 +1,42 @@ +## 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 + +## 2.0.8 + +* Updates the minimum Flutter version to 2.10, which is required by the change + in 2.0.7. + +## 2.0.7 + +* Marks the `Link` widget as invisible so it can be optimized by the engine. + +## 2.0.6 + +* Removes dependency on `meta`. + +## 2.0.5 + +* Updates code for new analysis options. + ## 2.0.4 -* Add `implements` to pubspec. +- Add `implements` to pubspec. ## 2.0.3 @@ -10,109 +46,109 @@ - Updated installation instructions in README. -# 2.0.1 +## 2.0.1 - Change sizing code of `Link` widget's `HtmlElementView` so it works well when slotted. -# 2.0.0 +## 2.0.0 - Migrate to null safety. -# 0.1.5+3 +## 0.1.5+3 - Fix Link misalignment [issue](https://github.com/flutter/flutter/issues/70053). -# 0.1.5+2 +## 0.1.5+2 - Update Flutter SDK constraint. -# 0.1.5+1 +## 0.1.5+1 - Substitute `undefined_prefixed_name: ignore` analyzer setting by a `dart:ui` shim with conditional exports. [Issue](https://github.com/flutter/flutter/issues/69309). -# 0.1.5 +## 0.1.5 - Added the web implementation of the Link widget. -# 0.1.4+2 +## 0.1.4+2 - Move `lib/third_party` to `lib/src/third_party`. -# 0.1.4+1 +## 0.1.4+1 - Add a more correct attribution to `package:platform_detect` code. -# 0.1.4 +## 0.1.4 - (Null safety) Remove dependency on `package:platform_detect` - Port unit tests to run with `flutter drive` -# 0.1.3+2 +## 0.1.3+2 - Fix a typo in a test name and fix some style inconsistencies. -# 0.1.3+1 +## 0.1.3+1 - Depend explicitly on the `platform_interface` package that adds the `webOnlyWindowName` parameter. -# 0.1.3 +## 0.1.3 - Added webOnlyWindowName parameter to launch() -# 0.1.2+1 +## 0.1.2+1 - Update docs -# 0.1.2 +## 0.1.2 - Adds "tel" and "sms" support -# 0.1.1+6 +## 0.1.1+6 - Open "mailto" urls with target set as "\_top" on Safari browsers. - Update lower bound of dart dependency to 2.2.0. -# 0.1.1+5 +## 0.1.1+5 - Update lower bound of dart dependency to 2.1.0. -# 0.1.1+4 +## 0.1.1+4 - Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). -# 0.1.1+3 +## 0.1.1+3 - Refactor tests to not rely on the underlying browser behavior. -# 0.1.1+2 +## 0.1.1+2 - Open urls with target "\_top" on iOS PWAs. -# 0.1.1+1 +## 0.1.1+1 - Make the pedantic dev_dependency explicit. -# 0.1.1 +## 0.1.1 - Added support for mailto scheme -# 0.1.0+2 +## 0.1.0+2 - Remove androidx references from the no-op android implemenation. -# 0.1.0+1 +## 0.1.0+1 - Add an android/ folder with no-op implementation to workaround https://github.com/flutter/flutter/issues/46304. - Bump the minimal required Flutter version to 1.10.0. -# 0.1.0 +## 0.1.0 - Update docs and pubspec. -# 0.0.2 +## 0.0.2 - Switch to using `url_launcher_platform_interface`. -# 0.0.1 +## 0.0.1 - Initial open-source release. 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 0487aca1c2dd..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 @@ -4,11 +4,13 @@ import 'dart:html' as html; import 'dart:js_util'; + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_web/src/link.dart'; -import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -50,6 +52,25 @@ void main() { // Check that the same anchor has been updated. expect(anchor.getAttribute('href'), uri2.toString()); expect(anchor.getAttribute('target'), '_self'); + + final Uri uri3 = Uri.parse('/foobar'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: uri3, + target: LinkTarget.self, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + await tester.pumpAndSettle(); + + // Check that internal route properly prepares using the default + // [UrlStrategy] + expect(anchor.getAttribute('href'), + urlStrategy?.prepareExternalUrl(uri3.toString())); + expect(anchor.getAttribute('target'), '_self'); }); testWidgets('sizes itself correctly', (WidgetTester tester) async { @@ -59,14 +80,14 @@ void main() { textDirection: TextDirection.ltr, child: Center( child: ConstrainedBox( - constraints: BoxConstraints.tight(Size(100.0, 100.0)), + constraints: BoxConstraints.tight(const Size(100.0, 100.0)), child: WebLinkDelegate(TestLinkInfo( uri: uri, target: LinkTarget.blank, builder: (BuildContext context, FollowLink? followLink) { return Container( key: containerKey, - child: SizedBox(width: 50.0, height: 50.0), + child: const SizedBox(width: 50.0, height: 50.0), ); }, )), @@ -102,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, + ); + }); }); } @@ -128,6 +179,12 @@ html.Element _findSingleAnchor() { } class TestLinkInfo extends LinkInfo { + TestLinkInfo({ + required this.uri, + required this.target, + required this.builder, + }); + @override final LinkWidgetBuilder builder; @@ -139,10 +196,4 @@ class TestLinkInfo extends LinkInfo { @override bool get isDisabled => uri == null; - - TestLinkInfo({ - required this.uri, - required this.target, - required this.builder, - }); } diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart index 0b53a1ffb1dd..10f5e0b40ffc 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart @@ -3,15 +3,16 @@ // found in the LICENSE file. import 'dart:html' as html; + import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; import 'package:mockito/mockito.dart'; -import 'package:integration_test/integration_test.dart'; +import 'package:url_launcher_web/url_launcher_web.dart'; import 'url_launcher_web_test.mocks.dart'; -@GenerateMocks([html.Window, html.Navigator]) +@GenerateMocks([html.Window, html.Navigator]) void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 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 9cd0196f51db..36903b0a4250 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,57 +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. - -// Mocks generated by Mockito 5.0.2 from annotations +// Mocks generated by Mockito 5.0.17 from annotations // in regular_integration_tests/integration_test/url_launcher_web_test.dart. // Do not manually edit this file. -import 'dart:async' as _i4; +import 'dart:async' as _i3; import 'dart:html' as _i2; -import 'dart:math' as _i5; -import 'dart:web_sql' as _i3; +import 'dart:math' as _i4; import 'package:mockito/mockito.dart' as _i1; +// 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 _FakeDocument extends _i1.Fake implements _i2.Document {} - -class _FakeLocation extends _i1.Fake implements _i2.Location {} +class _FakeDocument_0 extends _i1.Fake implements _i2.Document {} -class _FakeConsole extends _i1.Fake implements _i2.Console {} +class _FakeLocation_1 extends _i1.Fake implements _i2.Location {} -class _FakeHistory extends _i1.Fake implements _i2.History {} +class _FakeConsole_2 extends _i1.Fake implements _i2.Console {} -class _FakeStorage extends _i1.Fake implements _i2.Storage {} +class _FakeHistory_3 extends _i1.Fake implements _i2.History {} -class _FakeNavigator extends _i1.Fake implements _i2.Navigator {} +class _FakeStorage_4 extends _i1.Fake implements _i2.Storage {} -class _FakePerformance extends _i1.Fake implements _i2.Performance {} +class _FakeNavigator_5 extends _i1.Fake implements _i2.Navigator {} -class _FakeEvents extends _i1.Fake implements _i2.Events {} +class _FakePerformance_6 extends _i1.Fake implements _i2.Performance {} -class _FakeType extends _i1.Fake implements Type {} +class _FakeEvents_7 extends _i1.Fake implements _i2.Events {} -class _FakeWindowBase extends _i1.Fake implements _i2.WindowBase {} +class _FakeWindowBase_8 extends _i1.Fake implements _i2.WindowBase {} -class _FakeFileSystem extends _i1.Fake implements _i2.FileSystem {} +class _FakeFileSystem_9 extends _i1.Fake implements _i2.FileSystem {} -class _FakeStylePropertyMapReadonly extends _i1.Fake +class _FakeStylePropertyMapReadonly_10 extends _i1.Fake implements _i2.StylePropertyMapReadonly {} -class _FakeMediaQueryList extends _i1.Fake implements _i2.MediaQueryList {} - -class _FakeEntry extends _i1.Fake implements _i2.Entry {} +class _FakeMediaQueryList_11 extends _i1.Fake implements _i2.MediaQueryList {} -class _FakeSqlDatabase extends _i1.Fake implements _i3.SqlDatabase {} +class _FakeEntry_12 extends _i1.Fake implements _i2.Entry {} -class _FakeGeolocation extends _i1.Fake implements _i2.Geolocation {} +class _FakeGeolocation_13 extends _i1.Fake implements _i2.Geolocation {} -class _FakeMediaStream extends _i1.Fake implements _i2.MediaStream {} +class _FakeMediaStream_14 extends _i1.Fake implements _i2.MediaStream {} -class _FakeRelatedApplication extends _i1.Fake +class _FakeRelatedApplication_15 extends _i1.Fake implements _i2.RelatedApplication {} /// A class which mocks [Window]. @@ -63,37 +60,52 @@ class MockWindow extends _i1.Mock implements _i2.Window { } @override - _i4.Future get animationFrame => + _i3.Future get animationFrame => (super.noSuchMethod(Invocation.getter(#animationFrame), - returnValue: Future.value(0)) as _i4.Future); + returnValue: Future.value(0)) as _i3.Future); @override _i2.Document get document => (super.noSuchMethod(Invocation.getter(#document), - returnValue: _FakeDocument()) as _i2.Document); + returnValue: _FakeDocument_0()) as _i2.Document); @override _i2.Location get location => (super.noSuchMethod(Invocation.getter(#location), - returnValue: _FakeLocation()) as _i2.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()) as _i2.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()) as _i2.History); + returnValue: _FakeHistory_3()) as _i2.History); @override _i2.Storage get localStorage => (super.noSuchMethod(Invocation.getter(#localStorage), - returnValue: _FakeStorage()) as _i2.Storage); + 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()) as _i2.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) @@ -105,353 +117,357 @@ class MockWindow extends _i1.Mock implements _i2.Window { @override _i2.Performance get performance => (super.noSuchMethod(Invocation.getter(#performance), - returnValue: _FakePerformance()) as _i2.Performance); + returnValue: _FakePerformance_6()) as _i2.Performance); @override _i2.Storage get sessionStorage => (super.noSuchMethod(Invocation.getter(#sessionStorage), - returnValue: _FakeStorage()) as _i2.Storage); + returnValue: _FakeStorage_4()) as _i2.Storage); @override - _i4.Stream<_i2.Event> get onContentLoaded => + 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 _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onAbort => + _i3.Stream<_i2.Event> get onAbort => (super.noSuchMethod(Invocation.getter(#onAbort), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onBlur => + _i3.Stream<_i2.Event> get onBlur => (super.noSuchMethod(Invocation.getter(#onBlur), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onCanPlay => + _i3.Stream<_i2.Event> get onCanPlay => (super.noSuchMethod(Invocation.getter(#onCanPlay), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onCanPlayThrough => + _i3.Stream<_i2.Event> get onCanPlayThrough => (super.noSuchMethod(Invocation.getter(#onCanPlayThrough), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onChange => + _i3.Stream<_i2.Event> get onChange => (super.noSuchMethod(Invocation.getter(#onChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.MouseEvent> get onClick => + _i3.Stream<_i2.MouseEvent> get onClick => (super.noSuchMethod(Invocation.getter(#onClick), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onContextMenu => + _i3.Stream<_i2.MouseEvent> get onContextMenu => (super.noSuchMethod(Invocation.getter(#onContextMenu), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.Event> get onDoubleClick => + _i3.Stream<_i2.Event> get onDoubleClick => (super.noSuchMethod(Invocation.getter(#onDoubleClick), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => + _i3.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => (super.noSuchMethod(Invocation.getter(#onDeviceMotion), returnValue: Stream<_i2.DeviceMotionEvent>.empty()) - as _i4.Stream<_i2.DeviceMotionEvent>); + as _i3.Stream<_i2.DeviceMotionEvent>); @override - _i4.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => + _i3.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => (super.noSuchMethod(Invocation.getter(#onDeviceOrientation), returnValue: Stream<_i2.DeviceOrientationEvent>.empty()) - as _i4.Stream<_i2.DeviceOrientationEvent>); + as _i3.Stream<_i2.DeviceOrientationEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDrag => + _i3.Stream<_i2.MouseEvent> get onDrag => (super.noSuchMethod(Invocation.getter(#onDrag), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragEnd => + _i3.Stream<_i2.MouseEvent> get onDragEnd => (super.noSuchMethod(Invocation.getter(#onDragEnd), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragEnter => + _i3.Stream<_i2.MouseEvent> get onDragEnter => (super.noSuchMethod(Invocation.getter(#onDragEnter), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragLeave => + _i3.Stream<_i2.MouseEvent> get onDragLeave => (super.noSuchMethod(Invocation.getter(#onDragLeave), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragOver => + _i3.Stream<_i2.MouseEvent> get onDragOver => (super.noSuchMethod(Invocation.getter(#onDragOver), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDragStart => + _i3.Stream<_i2.MouseEvent> get onDragStart => (super.noSuchMethod(Invocation.getter(#onDragStart), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onDrop => + _i3.Stream<_i2.MouseEvent> get onDrop => (super.noSuchMethod(Invocation.getter(#onDrop), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.Event> get onDurationChange => + _i3.Stream<_i2.Event> get onDurationChange => (super.noSuchMethod(Invocation.getter(#onDurationChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onEmptied => + _i3.Stream<_i2.Event> get onEmptied => (super.noSuchMethod(Invocation.getter(#onEmptied), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onEnded => + _i3.Stream<_i2.Event> get onEnded => (super.noSuchMethod(Invocation.getter(#onEnded), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onError => + _i3.Stream<_i2.Event> get onError => (super.noSuchMethod(Invocation.getter(#onError), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onFocus => + _i3.Stream<_i2.Event> get onFocus => (super.noSuchMethod(Invocation.getter(#onFocus), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onHashChange => + _i3.Stream<_i2.Event> get onHashChange => (super.noSuchMethod(Invocation.getter(#onHashChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onInput => + _i3.Stream<_i2.Event> get onInput => (super.noSuchMethod(Invocation.getter(#onInput), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onInvalid => + _i3.Stream<_i2.Event> get onInvalid => (super.noSuchMethod(Invocation.getter(#onInvalid), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.KeyboardEvent> get onKeyDown => + _i3.Stream<_i2.KeyboardEvent> get onKeyDown => (super.noSuchMethod(Invocation.getter(#onKeyDown), returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i4.Stream<_i2.KeyboardEvent>); + as _i3.Stream<_i2.KeyboardEvent>); @override - _i4.Stream<_i2.KeyboardEvent> get onKeyPress => + _i3.Stream<_i2.KeyboardEvent> get onKeyPress => (super.noSuchMethod(Invocation.getter(#onKeyPress), returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i4.Stream<_i2.KeyboardEvent>); + as _i3.Stream<_i2.KeyboardEvent>); @override - _i4.Stream<_i2.KeyboardEvent> get onKeyUp => + _i3.Stream<_i2.KeyboardEvent> get onKeyUp => (super.noSuchMethod(Invocation.getter(#onKeyUp), returnValue: Stream<_i2.KeyboardEvent>.empty()) - as _i4.Stream<_i2.KeyboardEvent>); + as _i3.Stream<_i2.KeyboardEvent>); @override - _i4.Stream<_i2.Event> get onLoad => + _i3.Stream<_i2.Event> get onLoad => (super.noSuchMethod(Invocation.getter(#onLoad), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onLoadedData => + _i3.Stream<_i2.Event> get onLoadedData => (super.noSuchMethod(Invocation.getter(#onLoadedData), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onLoadedMetadata => + _i3.Stream<_i2.Event> get onLoadedMetadata => (super.noSuchMethod(Invocation.getter(#onLoadedMetadata), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onLoadStart => + _i3.Stream<_i2.Event> get onLoadStart => (super.noSuchMethod(Invocation.getter(#onLoadStart), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.MessageEvent> get onMessage => + _i3.Stream<_i2.MessageEvent> get onMessage => (super.noSuchMethod(Invocation.getter(#onMessage), returnValue: Stream<_i2.MessageEvent>.empty()) - as _i4.Stream<_i2.MessageEvent>); + as _i3.Stream<_i2.MessageEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseDown => + _i3.Stream<_i2.MouseEvent> get onMouseDown => (super.noSuchMethod(Invocation.getter(#onMouseDown), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseEnter => + _i3.Stream<_i2.MouseEvent> get onMouseEnter => (super.noSuchMethod(Invocation.getter(#onMouseEnter), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseLeave => + _i3.Stream<_i2.MouseEvent> get onMouseLeave => (super.noSuchMethod(Invocation.getter(#onMouseLeave), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseMove => + _i3.Stream<_i2.MouseEvent> get onMouseMove => (super.noSuchMethod(Invocation.getter(#onMouseMove), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseOut => + _i3.Stream<_i2.MouseEvent> get onMouseOut => (super.noSuchMethod(Invocation.getter(#onMouseOut), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseOver => + _i3.Stream<_i2.MouseEvent> get onMouseOver => (super.noSuchMethod(Invocation.getter(#onMouseOver), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.MouseEvent> get onMouseUp => + _i3.Stream<_i2.MouseEvent> get onMouseUp => (super.noSuchMethod(Invocation.getter(#onMouseUp), returnValue: Stream<_i2.MouseEvent>.empty()) - as _i4.Stream<_i2.MouseEvent>); + as _i3.Stream<_i2.MouseEvent>); @override - _i4.Stream<_i2.WheelEvent> get onMouseWheel => + _i3.Stream<_i2.WheelEvent> get onMouseWheel => (super.noSuchMethod(Invocation.getter(#onMouseWheel), returnValue: Stream<_i2.WheelEvent>.empty()) - as _i4.Stream<_i2.WheelEvent>); + as _i3.Stream<_i2.WheelEvent>); @override - _i4.Stream<_i2.Event> get onOffline => + _i3.Stream<_i2.Event> get onOffline => (super.noSuchMethod(Invocation.getter(#onOffline), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onOnline => + _i3.Stream<_i2.Event> get onOnline => (super.noSuchMethod(Invocation.getter(#onOnline), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPageHide => + _i3.Stream<_i2.Event> get onPageHide => (super.noSuchMethod(Invocation.getter(#onPageHide), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPageShow => + _i3.Stream<_i2.Event> get onPageShow => (super.noSuchMethod(Invocation.getter(#onPageShow), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPause => + _i3.Stream<_i2.Event> get onPause => (super.noSuchMethod(Invocation.getter(#onPause), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPlay => + _i3.Stream<_i2.Event> get onPlay => (super.noSuchMethod(Invocation.getter(#onPlay), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onPlaying => + _i3.Stream<_i2.Event> get onPlaying => (super.noSuchMethod(Invocation.getter(#onPlaying), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.PopStateEvent> get onPopState => + _i3.Stream<_i2.PopStateEvent> get onPopState => (super.noSuchMethod(Invocation.getter(#onPopState), returnValue: Stream<_i2.PopStateEvent>.empty()) - as _i4.Stream<_i2.PopStateEvent>); + as _i3.Stream<_i2.PopStateEvent>); @override - _i4.Stream<_i2.Event> get onProgress => + _i3.Stream<_i2.Event> get onProgress => (super.noSuchMethod(Invocation.getter(#onProgress), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onRateChange => + _i3.Stream<_i2.Event> get onRateChange => (super.noSuchMethod(Invocation.getter(#onRateChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onReset => + _i3.Stream<_i2.Event> get onReset => (super.noSuchMethod(Invocation.getter(#onReset), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onResize => + _i3.Stream<_i2.Event> get onResize => (super.noSuchMethod(Invocation.getter(#onResize), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onScroll => + _i3.Stream<_i2.Event> get onScroll => (super.noSuchMethod(Invocation.getter(#onScroll), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSearch => + _i3.Stream<_i2.Event> get onSearch => (super.noSuchMethod(Invocation.getter(#onSearch), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSeeked => + _i3.Stream<_i2.Event> get onSeeked => (super.noSuchMethod(Invocation.getter(#onSeeked), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSeeking => + _i3.Stream<_i2.Event> get onSeeking => (super.noSuchMethod(Invocation.getter(#onSeeking), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSelect => + _i3.Stream<_i2.Event> get onSelect => (super.noSuchMethod(Invocation.getter(#onSelect), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onStalled => + _i3.Stream<_i2.Event> get onStalled => (super.noSuchMethod(Invocation.getter(#onStalled), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.StorageEvent> get onStorage => + _i3.Stream<_i2.StorageEvent> get onStorage => (super.noSuchMethod(Invocation.getter(#onStorage), returnValue: Stream<_i2.StorageEvent>.empty()) - as _i4.Stream<_i2.StorageEvent>); + as _i3.Stream<_i2.StorageEvent>); @override - _i4.Stream<_i2.Event> get onSubmit => + _i3.Stream<_i2.Event> get onSubmit => (super.noSuchMethod(Invocation.getter(#onSubmit), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onSuspend => + _i3.Stream<_i2.Event> get onSuspend => (super.noSuchMethod(Invocation.getter(#onSuspend), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onTimeUpdate => + _i3.Stream<_i2.Event> get onTimeUpdate => (super.noSuchMethod(Invocation.getter(#onTimeUpdate), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.TouchEvent> get onTouchCancel => + _i3.Stream<_i2.TouchEvent> get onTouchCancel => (super.noSuchMethod(Invocation.getter(#onTouchCancel), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TouchEvent> get onTouchEnd => + _i3.Stream<_i2.TouchEvent> get onTouchEnd => (super.noSuchMethod(Invocation.getter(#onTouchEnd), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TouchEvent> get onTouchMove => + _i3.Stream<_i2.TouchEvent> get onTouchMove => (super.noSuchMethod(Invocation.getter(#onTouchMove), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TouchEvent> get onTouchStart => + _i3.Stream<_i2.TouchEvent> get onTouchStart => (super.noSuchMethod(Invocation.getter(#onTouchStart), returnValue: Stream<_i2.TouchEvent>.empty()) - as _i4.Stream<_i2.TouchEvent>); + as _i3.Stream<_i2.TouchEvent>); @override - _i4.Stream<_i2.TransitionEvent> get onTransitionEnd => + _i3.Stream<_i2.TransitionEvent> get onTransitionEnd => (super.noSuchMethod(Invocation.getter(#onTransitionEnd), returnValue: Stream<_i2.TransitionEvent>.empty()) - as _i4.Stream<_i2.TransitionEvent>); + as _i3.Stream<_i2.TransitionEvent>); @override - _i4.Stream<_i2.Event> get onUnload => + _i3.Stream<_i2.Event> get onUnload => (super.noSuchMethod(Invocation.getter(#onUnload), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onVolumeChange => + _i3.Stream<_i2.Event> get onVolumeChange => (super.noSuchMethod(Invocation.getter(#onVolumeChange), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.Event> get onWaiting => + _i3.Stream<_i2.Event> get onWaiting => (super.noSuchMethod(Invocation.getter(#onWaiting), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.AnimationEvent> get onAnimationEnd => + _i3.Stream<_i2.AnimationEvent> get onAnimationEnd => (super.noSuchMethod(Invocation.getter(#onAnimationEnd), returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i4.Stream<_i2.AnimationEvent>); + as _i3.Stream<_i2.AnimationEvent>); @override - _i4.Stream<_i2.AnimationEvent> get onAnimationIteration => + _i3.Stream<_i2.AnimationEvent> get onAnimationIteration => (super.noSuchMethod(Invocation.getter(#onAnimationIteration), returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i4.Stream<_i2.AnimationEvent>); + as _i3.Stream<_i2.AnimationEvent>); @override - _i4.Stream<_i2.AnimationEvent> get onAnimationStart => + _i3.Stream<_i2.AnimationEvent> get onAnimationStart => (super.noSuchMethod(Invocation.getter(#onAnimationStart), returnValue: Stream<_i2.AnimationEvent>.empty()) - as _i4.Stream<_i2.AnimationEvent>); + as _i3.Stream<_i2.AnimationEvent>); @override - _i4.Stream<_i2.Event> get onBeforeUnload => + _i3.Stream<_i2.Event> get onBeforeUnload => (super.noSuchMethod(Invocation.getter(#onBeforeUnload), - returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + returnValue: Stream<_i2.Event>.empty()) as _i3.Stream<_i2.Event>); @override - _i4.Stream<_i2.WheelEvent> get onWheel => + _i3.Stream<_i2.WheelEvent> get onWheel => (super.noSuchMethod(Invocation.getter(#onWheel), returnValue: Stream<_i2.WheelEvent>.empty()) - as _i4.Stream<_i2.WheelEvent>); + as _i3.Stream<_i2.WheelEvent>); @override int get pageXOffset => (super.noSuchMethod(Invocation.getter(#pageXOffset), returnValue: 0) @@ -468,18 +484,12 @@ class MockWindow extends _i1.Mock implements _i2.Window { (super.noSuchMethod(Invocation.getter(#scrollY), returnValue: 0) as int); @override _i2.Events get on => - (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents()) + (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents_7()) as _i2.Events); @override - int get hashCode => - (super.noSuchMethod(Invocation.getter(#hashCode), returnValue: 0) as int); - @override - Type get runtimeType => (super.noSuchMethod(Invocation.getter(#runtimeType), - returnValue: _FakeType()) as Type); - @override _i2.WindowBase open(String? url, String? name, [String? options]) => (super.noSuchMethod(Invocation.method(#open, [url, name, options]), - returnValue: _FakeWindowBase()) as _i2.WindowBase); + returnValue: _FakeWindowBase_8()) as _i2.WindowBase); @override int requestAnimationFrame(_i2.FrameRequestCallback? callback) => (super.noSuchMethod(Invocation.method(#requestAnimationFrame, [callback]), @@ -489,25 +499,32 @@ class MockWindow extends _i1.Mock implements _i2.Window { super.noSuchMethod(Invocation.method(#cancelAnimationFrame, [id]), returnValueForMissingStub: null); @override - _i4.Future<_i2.FileSystem> requestFileSystem(int? size, + _i3.Future<_i2.FileSystem> requestFileSystem(int? size, {bool? persistent = false}) => (super.noSuchMethod( Invocation.method( #requestFileSystem, [size], {#persistent: persistent}), - returnValue: Future.value(_FakeFileSystem())) - as _i4.Future<_i2.FileSystem>); + 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 - _i4.Future fetch(dynamic input, [Map? init]) => + _i3.Future fetch(dynamic input, [Map? init]) => (super.noSuchMethod(Invocation.method(#fetch, [input, init]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override bool find(String? string, bool? caseSensitive, bool? backwards, bool? wrap, bool? wholeWord, bool? searchInFrames, bool? showDialog) => @@ -527,7 +544,7 @@ class MockWindow extends _i1.Mock implements _i2.Window { _i2.Element? element, String? pseudoElement) => (super.noSuchMethod( Invocation.method(#getComputedStyleMap, [element, pseudoElement]), - returnValue: _FakeStylePropertyMapReadonly()) + returnValue: _FakeStylePropertyMapReadonly_10()) as _i2.StylePropertyMapReadonly); @override List<_i2.CssRule> getMatchedCssRules( @@ -538,7 +555,7 @@ class MockWindow extends _i1.Mock implements _i2.Window { @override _i2.MediaQueryList matchMedia(String? query) => (super.noSuchMethod(Invocation.method(#matchMedia, [query]), - returnValue: _FakeMediaQueryList()) as _i2.MediaQueryList); + returnValue: _FakeMediaQueryList_11()) as _i2.MediaQueryList); @override void moveBy(int? x, int? y) => super.noSuchMethod(Invocation.method(#moveBy, [x, y]), @@ -550,6 +567,9 @@ class MockWindow extends _i1.Mock implements _i2.Window { 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( @@ -564,9 +584,37 @@ class MockWindow extends _i1.Mock implements _i2.Window { super.noSuchMethod(Invocation.method(#resizeTo, [x, y]), returnValueForMissingStub: null); @override - _i4.Future<_i2.Entry> resolveLocalFileSystemUrl(String? url) => + void scroll( + [dynamic options_OR_x, + dynamic y, + Map? scrollOptions]) => + super.noSuchMethod( + 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.value(_FakeEntry())) as _i4.Future<_i2.Entry>); + returnValue: Future<_i2.Entry>.value(_FakeEntry_12())) + as _i3.Future<_i2.Entry>); @override String atob(String? atob) => (super.noSuchMethod(Invocation.method(#atob, [atob]), returnValue: '') @@ -576,18 +624,10 @@ class MockWindow extends _i1.Mock implements _i2.Window { (super.noSuchMethod(Invocation.method(#btoa, [btoa]), returnValue: '') as String); @override - void moveTo(_i5.Point? p) => + void moveTo(_i4.Point? p) => super.noSuchMethod(Invocation.method(#moveTo, [p]), returnValueForMissingStub: null); @override - _i3.SqlDatabase openDatabase(String? name, String? version, - String? displayName, int? estimatedSize, - [_i2.DatabaseCallback? creationCallback]) => - (super.noSuchMethod( - Invocation.method(#openDatabase, - [name, version, displayName, estimatedSize, creationCallback]), - returnValue: _FakeSqlDatabase()) as _i3.SqlDatabase); - @override void addEventListener(String? type, _i2.EventListener? listener, [bool? useCapture]) => super.noSuchMethod( @@ -603,14 +643,6 @@ class MockWindow extends _i1.Mock implements _i2.Window { bool dispatchEvent(_i2.Event? event) => (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), returnValue: false) as bool); - @override - bool operator ==(Object? other) => - (super.noSuchMethod(Invocation.method(#==, [other]), returnValue: false) - as bool); - @override - String toString() => - (super.noSuchMethod(Invocation.method(#toString, []), returnValue: '') - as String); } /// A class which mocks [Navigator]. @@ -628,7 +660,7 @@ class MockNavigator extends _i1.Mock implements _i2.Navigator { @override _i2.Geolocation get geolocation => (super.noSuchMethod(Invocation.getter(#geolocation), - returnValue: _FakeGeolocation()) as _i2.Geolocation); + returnValue: _FakeGeolocation_13()) as _i2.Geolocation); @override String get vendor => (super.noSuchMethod(Invocation.getter(#vendor), returnValue: '') @@ -658,69 +690,61 @@ class MockNavigator extends _i1.Mock implements _i2.Navigator { (super.noSuchMethod(Invocation.getter(#userAgent), returnValue: '') as String); @override - int get hashCode => - (super.noSuchMethod(Invocation.getter(#hashCode), returnValue: 0) as int); - @override - Type get runtimeType => (super.noSuchMethod(Invocation.getter(#runtimeType), - returnValue: _FakeType()) as Type); - @override List<_i2.Gamepad?> getGamepads() => (super.noSuchMethod(Invocation.method(#getGamepads, []), returnValue: <_i2.Gamepad?>[]) as List<_i2.Gamepad?>); @override - _i4.Future<_i2.MediaStream> getUserMedia( + _i3.Future<_i2.MediaStream> getUserMedia( {dynamic audio = false, dynamic video = false}) => (super.noSuchMethod( Invocation.method(#getUserMedia, [], {#audio: audio, #video: video}), returnValue: - Future.value(_FakeMediaStream())) as _i4.Future<_i2.MediaStream>); + Future<_i2.MediaStream>.value(_FakeMediaStream_14())) as _i3 + .Future<_i2.MediaStream>); @override - _i4.Future getBattery() => + void cancelKeyboardLock() => + super.noSuchMethod(Invocation.method(#cancelKeyboardLock, []), + returnValueForMissingStub: null); + @override + _i3.Future getBattery() => (super.noSuchMethod(Invocation.method(#getBattery, []), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - _i4.Future<_i2.RelatedApplication> getInstalledRelatedApps() => + _i3.Future<_i2.RelatedApplication> getInstalledRelatedApps() => (super.noSuchMethod(Invocation.method(#getInstalledRelatedApps, []), - returnValue: Future.value(_FakeRelatedApplication())) - as _i4.Future<_i2.RelatedApplication>); + returnValue: Future<_i2.RelatedApplication>.value( + _FakeRelatedApplication_15())) + as _i3.Future<_i2.RelatedApplication>); @override - _i4.Future getVRDisplays() => + _i3.Future getVRDisplays() => (super.noSuchMethod(Invocation.method(#getVRDisplays, []), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override void registerProtocolHandler(String? scheme, String? url, String? title) => super.noSuchMethod( Invocation.method(#registerProtocolHandler, [scheme, url, title]), returnValueForMissingStub: null); @override - _i4.Future requestKeyboardLock([List? keyCodes]) => + _i3.Future requestKeyboardLock([List? keyCodes]) => (super.noSuchMethod(Invocation.method(#requestKeyboardLock, [keyCodes]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - _i4.Future requestMidiAccess([Map? options]) => + _i3.Future requestMidiAccess([Map? options]) => (super.noSuchMethod(Invocation.method(#requestMidiAccess, [options]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override - _i4.Future requestMediaKeySystemAccess(String? keySystem, + _i3.Future requestMediaKeySystemAccess(String? keySystem, List>? supportedConfigurations) => (super.noSuchMethod( Invocation.method(#requestMediaKeySystemAccess, [keySystem, supportedConfigurations]), - returnValue: Future.value(null)) as _i4.Future); + returnValue: Future.value()) as _i3.Future); @override bool sendBeacon(String? url, Object? data) => (super.noSuchMethod(Invocation.method(#sendBeacon, [url, data]), returnValue: false) as bool); @override - _i4.Future share([Map? data]) => + _i3.Future share([Map? data]) => (super.noSuchMethod(Invocation.method(#share, [data]), - returnValue: Future.value(null)) as _i4.Future); - @override - bool operator ==(Object? other) => - (super.noSuchMethod(Invocation.method(#==, [other]), returnValue: false) - as bool); - @override - String toString() => - (super.noSuchMethod(Invocation.method(#toString, []), returnValue: '') - as String); + returnValue: 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 e1a38dcdcd46..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,19 +5,22 @@ 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 { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml index 7c00c33550a8..a25f4ce148e9 100644 --- a/packages/url_launcher/url_launcher_web/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -3,20 +3,20 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter dev_dependencies: - build_runner: ^1.10.0 - mockito: ^5.0.0 - url_launcher_web: - path: ../ + build_runner: ^2.1.1 flutter_driver: sdk: flutter flutter_test: sdk: flutter integration_test: sdk: flutter + mockito: ^5.0.0 + 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 3c556b3950b0..112d07ea7571 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -11,7 +11,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; - +import 'package:flutter_web_plugins/flutter_web_plugins.dart' show urlStrategy; import 'package:url_launcher_platform_interface/link.dart'; /// The unique identifier for the view type to be used for link platform views. @@ -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); @@ -85,8 +85,8 @@ class WebLinkDelegateState extends State { (BuildContext context, PlatformViewController controller) { return PlatformViewSurface( controller: controller, - gestureRecognizers: - Set>(), + gestureRecognizers: const < + Factory>{}, hitTestBehavior: PlatformViewHitTestBehavior.transparent, ); }, @@ -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,17 +113,23 @@ 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; } - static Map _instances = {}; + static final Map _instances = + {}; static html.Element _viewFactory(int viewId) { return _instances[viewId]!._element; @@ -131,7 +137,7 @@ class LinkViewController extends PlatformViewController { static int? _hitTestedViewId; - static late StreamSubscription _clickSubscription; + static late StreamSubscription _clickSubscription; static void _onGlobalClick(html.MouseEvent event) { final int? viewId = getViewIdFromTarget(event); @@ -158,10 +164,8 @@ 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; Future _initialize() async { @@ -206,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; @@ -220,7 +224,13 @@ class LinkViewController extends PlatformViewController { if (uri == null) { _element.removeAttribute('href'); } else { - _element.setAttribute('href', uri.toString()); + String href = uri.toString(); + // in case an internal uri is given, the url mus be properly encoded + // using the currently used [UrlStrategy] + if (!uri.hasScheme) { + href = urlStrategy?.prepareExternalUrl(href) ?? href; + } + _element.setAttribute('href', href); } } @@ -271,6 +281,10 @@ class LinkViewController extends PlatformViewController { int? getViewIdFromTarget(html.Event event) { final html.Element? linkElement = getLinkElementFromTarget(event); if (linkElement != null) { + // TODO(stuartmorgan): Remove this ignore (and change to getProperty) + // once the templated version is available on stable. On master (2.8) this + // is already not necessary. + // ignore: return_of_invalid_type return getProperty(linkElement, linkViewIdProperty); } return null; diff --git a/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui.dart b/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui.dart index 5eacec5fe867..0f6cd89dd288 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui.dart +++ b/packages/url_launcher/url_launcher_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 -// are exposed from a dedicated place. +// TODO(ditman): Remove this file once web-only dart:ui APIs +// are exposed from a dedicated place, flutter/flutter#55000. export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; 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 f2862af8b704..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 @@ -7,21 +7,27 @@ 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, + {bool isVisible = true}) { + 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/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart index 9e83c391de0b..6935cb55df77 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart @@ -26,8 +26,8 @@ import 'dart:html' as html show Navigator; /// Determines if the `navigator` is Safari. bool navigatorIsSafari(html.Navigator navigator) { // An web view running in an iOS app does not have a 'Version/X.X.X' string in the appVersion - final vendor = navigator.vendor; - final appVersion = navigator.appVersion; + final String vendor = navigator.vendor; + final String appVersion = navigator.appVersion; return vendor != null && vendor.contains('Apple') && appVersion != null && 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 9249837bd46b..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 @@ -4,17 +4,17 @@ import 'dart:async'; import 'dart:html' as html; -import 'src/shims/dart_ui.dart' as ui; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:meta/meta.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'src/link.dart'; +import 'src/shims/dart_ui.dart' as ui; import 'src/third_party/platform_detect/browser.dart'; -const _safariTargetTopSchemes = { +const Set _safariTargetTopSchemes = { 'mailto', 'tel', 'sms', @@ -28,25 +28,26 @@ bool _isSafariTargetTopScheme(String url) => /// /// This class implements the `package:url_launcher` functionality for the web. class UrlLauncherPlugin extends UrlLauncherPlatform { - html.Window _window; + /// A constructor that allows tests to override the window object used by the plugin. + UrlLauncherPlugin({@visibleForTesting html.Window? debugWindow}) + : _window = debugWindow ?? html.window { + _isSafari = navigatorIsSafari(_window.navigator); + } + + final html.Window _window; bool _isSafari = false; // The set of schemes that can be handled by the plugin - static final _supportedSchemes = { + static final Set _supportedSchemes = { 'http', 'https', }.union(_safariTargetTopSchemes); - /// A constructor that allows tests to override the window object used by the plugin. - UrlLauncherPlugin({@visibleForTesting html.Window? debugWindow}) - : _window = debugWindow ?? html.window { - _isSafari = navigatorIsSafari(_window.navigator); - } - /// Registers this class as the default instance of [UrlLauncherPlatform]. static void registerWith(Registrar registrar) { UrlLauncherPlatform.instance = UrlLauncherPlugin(); - ui.platformViewRegistry.registerViewFactory(linkViewType, linkViewFactory); + ui.platformViewRegistry + .registerViewFactory(linkViewType, linkViewFactory, isVisible: false); } @override @@ -61,8 +62,9 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { html.WindowBase openNewWindow(String url, {String? webOnlyWindowName}) { // We need to open mailto, tel and sms urls on the _top window context on safari browsers. // See https://github.com/flutter/flutter/issues/51461 for reference. - final target = webOnlyWindowName ?? + 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 77e8068f1396..d0e0fa905d57 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -1,12 +1,12 @@ name: url_launcher_web description: Web platform implementation of url_launcher -repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web +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.4 +version: 2.0.12 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.10.0" flutter: plugin: @@ -21,10 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.3.0 # null safe url_launcher_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index d095a52341b5..3ff14fd2f18a 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,6 +1,22 @@ -## NEXT +## 3.0.1 -* Added unit tests. +* 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 + existing versions of `url_launcher`, requiring Dart registration in this + package is in practice a breaking change. + * Does not include any API changes; clients can allow both 2.x or 3.x. + +## 2.0.3 + +**\[Retracted\]** + +* Switches to an in-package method channel implementation. +* Adds unit tests. +* Updates code for new analysis options. ## 2.0.2 diff --git a/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart index ae9a9148f9d7..c9d0d8c9c096 100644 --- a/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart @@ -10,7 +10,7 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('canLaunch', (WidgetTester _) async { - UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + final UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; expect(await launcher.canLaunch('randomstring'), false); diff --git a/packages/url_launcher/url_launcher_windows/example/lib/main.dart b/packages/url_launcher/url_launcher_windows/example/lib/main.dart index 86e06f3fafed..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( @@ -20,17 +22,17 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'URL Launcher'), + home: const MyHomePage(title: 'URL Launcher'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({Key? key, required this.title}) : super(key: key); 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 11be3e84f03b..22b524df2488 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.8.0" dependencies: flutter: @@ -19,11 +19,10 @@ dependencies: path: ../ dev_dependencies: - integration_test: - sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.10.0 + integration_test: + sdk: flutter flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt index 5b1622bcb333..5a5d2e8034b2 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt @@ -48,6 +48,8 @@ add_subdirectory("runner") # Enable the test target. set(include_url_launcher_windows_tests TRUE) +# Provide an alias for the test target using the name expected by repo tooling. +add_custom_target(unit_tests DEPENDS url_launcher_windows_test) # Generated plugin build rules, which manage building the plugins and adding # them to the application. diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4f7884874da7..000000000000 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a931..000000000000 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/main.cpp b/packages/url_launcher/url_launcher_windows/example/windows/runner/main.cpp index 126302b0be18..c7dbde1c7123 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/runner/main.cpp +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/main.cpp @@ -11,7 +11,7 @@ #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _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()) { diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.cpp b/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.cpp index 537728149601..e875ce8b05a9 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.cpp +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.cpp @@ -13,7 +13,7 @@ void CreateAndAttachConsole() { if (::AllocConsole()) { - FILE *unused; + FILE* unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart new file mode 100644 index 000000000000..b0ee8cb1a0b4 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/url_launcher_windows'); + +/// An implementation of [UrlLauncherPlatform] for Windows. +class UrlLauncherWindows extends UrlLauncherPlatform { + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void registerWith() { + UrlLauncherPlatform.instance = UrlLauncherWindows(); + } + + @override + final LinkDelegate? linkDelegate = null; + + @override + Future canLaunch(String url) { + return _channel.invokeMethod( + 'canLaunch', + {'url': url}, + ).then((bool? value) => value ?? false); + } + + @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, + }) { + return _channel.invokeMethod( + 'launch', + { + 'url': url, + 'enableJavaScript': enableJavaScript, + 'enableDomStorage': enableDomStorage, + 'universalLinksOnly': universalLinksOnly, + 'headers': headers, + }, + ).then((bool? value) => value ?? false); + } +} diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index a92e91ee4568..2717e3807e21 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -1,12 +1,12 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows +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: 2.0.2 +version: 3.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -14,7 +14,14 @@ flutter: platforms: windows: pluginClass: UrlLauncherWindows + dartPluginClass: UrlLauncherWindows dependencies: flutter: sdk: flutter + url_launcher_platform_interface: ^2.0.3 + +dev_dependencies: + flutter_test: + sdk: flutter + test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart new file mode 100644 index 000000000000..8b55b29bb530 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.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:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'package:url_launcher_windows/url_launcher_windows.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$UrlLauncherWindows', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/url_launcher_windows'); + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + test('registers instance', () { + UrlLauncherWindows.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); + + tearDown(() { + log.clear(); + }); + + test('canLaunch', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(); + await launcher.canLaunch('http://example.com/'); + expect( + log, + [ + isMethodCall('canLaunch', arguments: { + 'url': 'http://example.com/', + }) + ], + ); + }); + + test('canLaunch should return false if platform returns null', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(); + final bool canLaunch = await launcher.canLaunch('http://example.com/'); + + expect(canLaunch, false); + }); + + test('launch', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch with headers', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(); + await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {'key': 'value'}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {'key': 'value'}, + }) + ], + ); + }); + + test('launch universal links only', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(); + await launcher.launch( + 'http://example.com/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: const {}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': true, + 'headers': {}, + }) + ], + ); + }); + + test('launch should return false if platform returns null', () async { + final UrlLauncherWindows launcher = UrlLauncherWindows(); + final bool launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp index 748c75ddd243..d5f201219c75 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp @@ -63,7 +63,7 @@ std::string GetUrlArgument(const flutter::MethodCall<>& method_call) { void UrlLauncherPlugin::RegisterWithRegistrar( flutter::PluginRegistrar* registrar) { auto channel = std::make_unique>( - registrar->messenger(), "plugins.flutter.io/url_launcher", + registrar->messenger(), "plugins.flutter.io/url_launcher_windows", &flutter::StandardMethodCodec::GetInstance()); std::unique_ptr plugin = diff --git a/packages/video_player/analysis_options.yaml b/packages/video_player/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/video_player/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/video_player/video_player/AUTHORS b/packages/video_player/video_player/AUTHORS index 493a0b4ef9c2..02a9c690f330 100644 --- a/packages/video_player/video_player/AUTHORS +++ b/packages/video_player/video_player/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Koen Van Looveren diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index de60af49b95d..2b0ffadaecdf 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,106 @@ +## 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 (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. + +## 2.2.18 + +* Moves Android and iOS implementations to federated packages. +* Update audio URL in iOS tests. + +## 2.2.17 + +* Avoid blocking the main thread loading video count on iOS. + +## 2.2.16 + +* Introduces `setCaptionOffset` to offset the caption display based on a Duration. + +## 2.2.15 + +* Updates README discussion of permissions. + +## 2.2.14 + +* Removes KVO observer on AVPlayerItem on iOS. + +## 2.2.13 + +* Fixes persisting of hasError even after successful initialize. + +## 2.2.12 + +* iOS: Validate size only when assets contain video tracks. + +## 2.2.11 + +* Removes dependency on `meta`. + +## 2.2.10 + +* iOS: Updates texture on `seekTo`. + +## 2.2.9 + +* Adds compatibility with `video_player_platform_interface` 5.0, which does not + include non-dev test dependencies. + +## 2.2.8 + +* Changes the way the `VideoPlayerPlatform` instance is cached in the + controller, so that it's no longer impossible to change after the first use. +* Updates unit tests to be self-contained. +* Fixes integration tests. +* Updates Android compileSdkVersion to 31. +* Fixes a flaky integration test. +* Integration tests now use WebM on web, to allow running with Chromium. + +## 2.2.7 + +* Fixes a regression where dragging a [VideoProgressIndicator] while playing + would restart playback from the start of the video. + +## 2.2.6 + +* Initialize player when size and duration become available on iOS + +## 2.2.5 + +* Support to closed caption WebVTT format added. + ## 2.2.4 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. diff --git a/packages/video_player/video_player/CONTRIBUTING.md b/packages/video_player/video_player/CONTRIBUTING.md deleted file mode 100644 index 15c48038f6fc..000000000000 --- a/packages/video_player/video_player/CONTRIBUTING.md +++ /dev/null @@ -1,82 +0,0 @@ -## Updating pigeon-generated files - -If you update files in the pigeons/ directory, run the following -command in this directory (ignore the errors you get about -dependencies in the examples directory): - -```bash -flutter pub upgrade -flutter pub run pigeon --dart_null_safety --input pigeons/messages.dart -# git commit your changes so that your working environment is clean -(cd ../../../; ./script/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). - -While contributing, you may also want to set the following dependency -overrides: - -```yaml -dependency_overrides: - video_player_platform_interface: - path: - ../video_player_platform_interface - video_player_web: - path: - ../video_player_web -``` - -## Publishing plugin updates that span multiple plugin packages - -If your change affects both the interface package and the -implementation packages, then you will need to publish a version of -the plugin in between landing the interface changes and the -implementation changes, since the implementations depend on the -interface via pub. - -To do this, follow these steps: - -1. Create a PR that has all the changes, and update the -`pubspec.yaml`s to have path-based dependency overrides as described -in the "Updating pigeon-generated files" section above. - -2. Upload that PR and get it reviewed and into a state where the only -test failure is the one complaining that you can't publish a package -that has dependency overrides. - -3. Create a PR that's a subset of the one in the previous step that -only includes the interface changes, with no dependency overrides, and -submit that. - -4. Once you have had that reviewed and landed, publish the interface -parts of the plugin to pub. - -5. Now, update the original full PR to not use dependency overrides -but to instead refer to the new version of the plugin, and sync it to -master (so that the interface changes are gone from the PR). Submit -that PR. - -6. Once you have had _that_ PR reviewed and landed, publish the -implementation parts of the plugin to pub. - -You may need to publish each implementation package independently of -the main package also, depending on exactly what your change entails. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index d5e7528fa973..c870688a4fee 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -4,7 +4,11 @@ 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 @@ -12,35 +16,26 @@ First, add `video_player` as a [dependency in your pubspec.yaml file](https://fl ### iOS -This plugin requires iOS 9.0 or higher. Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: - -```xml -NSAppTransportSecurity - - NSAllowsArbitraryLoads - - -``` - -This entry allows your app to access video files by URL. +If you need to access videos using `http` (rather than `https`) URLs, you will need to add +the appropriate `NSAppTransportSecurity` permissions to your app's _Info.plist_ file, located +in `/ios/Runner/Info.plist`. See +[Apple's documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity) +to determine the right combination of entries for your use case and supported iOS versions. ### Android -Ensure the following permission is present in your Android Manifest file, located in `/android/app/src/main/AndroidManifest.xml`: +If you are using network-based videos, ensure that the following permission is present in your +Android Manifest file, located in `/android/app/src/main/AndroidManifest.xml`: ```xml ``` -The Flutter project template adds it, so it may already be there. - ### Web -This plugin compiles for the web platform since version `0.10.5`, in recent enough versions of Flutter (`>=1.12.13+hotfix.4`). - > 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. @@ -128,7 +123,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/android/build.gradle b/packages/video_player/video_player/android/build.gradle deleted file mode 100644 index 5d6b737f47a5..000000000000 --- a/packages/video_player/video_player/android/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -group 'io.flutter.plugins.videoplayer' -version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - 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' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/video_player/video_player/android/settings.gradle b/packages/video_player/video_player/android/settings.gradle deleted file mode 100644 index bbc9b9dd21d8..000000000000 --- a/packages/video_player/video_player/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'video_player' diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java deleted file mode 100644 index e0a4a3b8dd08..000000000000 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ /dev/null @@ -1,621 +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 - -package io.flutter.plugins.videoplayer; - -import io.flutter.plugin.common.BasicMessageChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.StandardMessageCodec; -import java.util.HashMap; - -/** Generated class from Pigeon. */ -@SuppressWarnings("unused") -public class Messages { - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class TextureMessage { - private Long textureId; - - public Long getTextureId() { - return textureId; - } - - public void setTextureId(Long setterArg) { - this.textureId = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - return toMapResult; - } - - static TextureMessage fromMap(HashMap map) { - TextureMessage fromMapResult = new TextureMessage(); - Object textureId = map.get("textureId"); - fromMapResult.textureId = - (textureId == null) - ? null - : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); - return fromMapResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class CreateMessage { - private String asset; - - public String getAsset() { - return asset; - } - - public void setAsset(String setterArg) { - this.asset = setterArg; - } - - private String uri; - - public String getUri() { - return uri; - } - - public void setUri(String setterArg) { - this.uri = setterArg; - } - - private String packageName; - - public String getPackageName() { - return packageName; - } - - public void setPackageName(String setterArg) { - this.packageName = setterArg; - } - - private String formatHint; - - public String getFormatHint() { - return formatHint; - } - - public void setFormatHint(String setterArg) { - this.formatHint = setterArg; - } - - private HashMap httpHeaders; - - public HashMap getHttpHeaders() { - return httpHeaders; - } - - public void setHttpHeaders(HashMap setterArg) { - this.httpHeaders = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("asset", asset); - toMapResult.put("uri", uri); - toMapResult.put("packageName", packageName); - toMapResult.put("formatHint", formatHint); - toMapResult.put("httpHeaders", httpHeaders); - return toMapResult; - } - - static CreateMessage fromMap(HashMap map) { - CreateMessage fromMapResult = new CreateMessage(); - Object asset = map.get("asset"); - fromMapResult.asset = (String) asset; - Object uri = map.get("uri"); - fromMapResult.uri = (String) uri; - Object packageName = map.get("packageName"); - fromMapResult.packageName = (String) packageName; - Object formatHint = map.get("formatHint"); - fromMapResult.formatHint = (String) formatHint; - Object httpHeaders = map.get("httpHeaders"); - fromMapResult.httpHeaders = (HashMap) httpHeaders; - return fromMapResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class LoopingMessage { - private Long textureId; - - public Long getTextureId() { - return textureId; - } - - public void setTextureId(Long setterArg) { - this.textureId = setterArg; - } - - private Boolean isLooping; - - public Boolean getIsLooping() { - return isLooping; - } - - public void setIsLooping(Boolean setterArg) { - this.isLooping = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("isLooping", isLooping); - return toMapResult; - } - - static LoopingMessage fromMap(HashMap map) { - LoopingMessage fromMapResult = new LoopingMessage(); - Object textureId = map.get("textureId"); - fromMapResult.textureId = - (textureId == null) - ? null - : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); - Object isLooping = map.get("isLooping"); - fromMapResult.isLooping = (Boolean) isLooping; - return fromMapResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class VolumeMessage { - private Long textureId; - - public Long getTextureId() { - return textureId; - } - - public void setTextureId(Long setterArg) { - this.textureId = setterArg; - } - - private Double volume; - - public Double getVolume() { - return volume; - } - - public void setVolume(Double setterArg) { - this.volume = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("volume", volume); - return toMapResult; - } - - static VolumeMessage fromMap(HashMap map) { - VolumeMessage fromMapResult = new VolumeMessage(); - Object textureId = map.get("textureId"); - fromMapResult.textureId = - (textureId == null) - ? null - : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); - Object volume = map.get("volume"); - fromMapResult.volume = (Double) volume; - return fromMapResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class PlaybackSpeedMessage { - private Long textureId; - - public Long getTextureId() { - return textureId; - } - - public void setTextureId(Long setterArg) { - this.textureId = setterArg; - } - - private Double speed; - - public Double getSpeed() { - return speed; - } - - public void setSpeed(Double setterArg) { - this.speed = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("speed", speed); - return toMapResult; - } - - static PlaybackSpeedMessage fromMap(HashMap map) { - PlaybackSpeedMessage fromMapResult = new PlaybackSpeedMessage(); - Object textureId = map.get("textureId"); - fromMapResult.textureId = - (textureId == null) - ? null - : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); - Object speed = map.get("speed"); - fromMapResult.speed = (Double) speed; - return fromMapResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class PositionMessage { - private Long textureId; - - public Long getTextureId() { - return textureId; - } - - public void setTextureId(Long setterArg) { - this.textureId = setterArg; - } - - private Long position; - - public Long getPosition() { - return position; - } - - public void setPosition(Long setterArg) { - this.position = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("textureId", textureId); - toMapResult.put("position", position); - return toMapResult; - } - - static PositionMessage fromMap(HashMap map) { - PositionMessage fromMapResult = new PositionMessage(); - Object textureId = map.get("textureId"); - fromMapResult.textureId = - (textureId == null) - ? null - : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); - Object position = map.get("position"); - fromMapResult.position = - (position == null) - ? null - : ((position instanceof Integer) ? (Integer) position : (Long) position); - return fromMapResult; - } - } - - /** Generated class from Pigeon that represents data sent in messages. */ - public static class MixWithOthersMessage { - private Boolean mixWithOthers; - - public Boolean getMixWithOthers() { - return mixWithOthers; - } - - public void setMixWithOthers(Boolean setterArg) { - this.mixWithOthers = setterArg; - } - - HashMap toMap() { - HashMap toMapResult = new HashMap<>(); - toMapResult.put("mixWithOthers", mixWithOthers); - return toMapResult; - } - - static MixWithOthersMessage fromMap(HashMap map) { - MixWithOthersMessage fromMapResult = new MixWithOthersMessage(); - Object mixWithOthers = map.get("mixWithOthers"); - fromMapResult.mixWithOthers = (Boolean) mixWithOthers; - return fromMapResult; - } - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface VideoPlayerApi { - 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); - - /** Sets up an instance of `VideoPlayerApi` to handle messages through the `binaryMessenger` */ - static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.initialize", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - api.initialize(); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.create", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - CreateMessage input = CreateMessage.fromMap((HashMap) message); - TextureMessage output = api.create(input); - wrapped.put("result", output.toMap()); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.dispose", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - TextureMessage input = TextureMessage.fromMap((HashMap) message); - api.dispose(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.setLooping", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - LoopingMessage input = LoopingMessage.fromMap((HashMap) message); - api.setLooping(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.setVolume", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - VolumeMessage input = VolumeMessage.fromMap((HashMap) message); - api.setVolume(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - PlaybackSpeedMessage input = PlaybackSpeedMessage.fromMap((HashMap) message); - api.setPlaybackSpeed(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.play", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - TextureMessage input = TextureMessage.fromMap((HashMap) message); - api.play(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.position", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - TextureMessage input = TextureMessage.fromMap((HashMap) message); - PositionMessage output = api.position(input); - wrapped.put("result", output.toMap()); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.seekTo", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - PositionMessage input = PositionMessage.fromMap((HashMap) message); - api.seekTo(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.pause", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - TextureMessage input = TextureMessage.fromMap((HashMap) message); - api.pause(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers", - new StandardMessageCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - HashMap wrapped = new HashMap<>(); - try { - @SuppressWarnings("ConstantConditions") - MixWithOthersMessage input = MixWithOthersMessage.fromMap((HashMap) message); - api.setMixWithOthers(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - } - } - - private static HashMap wrapError(Exception exception) { - HashMap errorMap = new HashMap<>(); - errorMap.put("message", exception.toString()); - errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put("details", null); - return errorMap; - } -} diff --git a/packages/video_player/video_player/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java deleted file mode 100644 index ec960b7a4480..000000000000 --- a/packages/video_player/video_player/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.videoplayer; - -import org.junit.Test; - -public class VideoPlayerTest { - // 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/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.iml b/packages/video_player/video_player/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/video_player/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - 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 0d1d5031ef4f..7b3c7db80c7e 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -37,7 +37,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.videoplayerexample" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -58,9 +58,9 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' + testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'org.mockito:mockito-core:3.5.13' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - testImplementation 'org.robolectric:robolectric:3.8' - testImplementation 'org.mockito:mockito-core:3.5.13' } diff --git a/packages/video_player/video_player/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/video_player/video_player/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/video_player/video_player/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/video_player/video_player/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java b/packages/video_player/video_player/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..45cf5c6e9903 --- /dev/null +++ b/packages/video_player/video_player/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/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.videoplayerexample; + +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/video_player/video_player/example/android/build.gradle b/packages/video_player/video_player/example/android/build.gradle index 456d020f6e2c..c21bff8e0a2f 100644 --- a/packages/video_player/video_player/example/android/build.gradle +++ b/packages/video_player/video_player/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/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7318..b8793d3c0d69 100644 --- a/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player/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/video_player/video_player/example/assets/Audio.mp3 b/packages/video_player/video_player/example/assets/Audio.mp3 new file mode 100644 index 000000000000..355eb9b2e1fb Binary files /dev/null and b/packages/video_player/video_player/example/assets/Audio.mp3 differ diff --git a/packages/video_player/video_player/example/assets/Butterfly-209.webm b/packages/video_player/video_player/example/assets/Butterfly-209.webm new file mode 100644 index 000000000000..991bdc7108cc Binary files /dev/null and b/packages/video_player/video_player/example/assets/Butterfly-209.webm differ diff --git a/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt b/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt new file mode 100644 index 000000000000..1dca2c58695e --- /dev/null +++ b/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt @@ -0,0 +1,7 @@ +WEBVTT + +00:00:00.200 --> 00:00:01.750 +[ Birds chirping ] + +00:00:02.300 --> 00:00:05.000 +[ Buzzing ] 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 cae51767f4aa..f80e600bcff5 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 @@ -6,8 +6,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:video_player/video_player.dart'; const Duration _playDuration = Duration(seconds: 1); @@ -17,29 +17,31 @@ void main() { testWidgets( 'can substitute one controller by another without crashing', (WidgetTester tester) async { - VideoPlayerController controller = VideoPlayerController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + // Use WebM for web to allow CI to use Chromium. + const String videoAssetKey = + kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4'; + + final VideoPlayerController controller = VideoPlayerController.asset( + videoAssetKey, ); - VideoPlayerController another = VideoPlayerController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + final VideoPlayerController another = VideoPlayerController.asset( + videoAssetKey, ); await controller.initialize(); await another.initialize(); await controller.setVolume(0); await another.setVolume(0); - final Completer started = Completer(); - final Completer ended = Completer(); - bool startedBuffering = false; - bool endedBuffering = false; + final Completer started = Completer(); + final Completer ended = Completer(); another.addListener(() { - if (another.value.isBuffering && !startedBuffering) { - startedBuffering = true; + if (another.value.isBuffering && !started.isCompleted) { started.complete(); } - if (startedBuffering && !another.value.isBuffering && !endedBuffering) { - endedBuffering = true; + if (started.isCompleted && + !another.value.isBuffering && + !ended.isCompleted) { ended.complete(); } }); @@ -65,11 +67,8 @@ void main() { expect(another.value.position, (Duration position) => position > const Duration(seconds: 0)); - await started; - expect(startedBuffering, true); - - await ended; - expect(endedBuffering, true); + await expectLater(started.future, completes); + await expectLater(ended.future, completes); }, skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), ); @@ -82,7 +81,7 @@ Widget renderVideoWidget(VideoPlayerController controller) { textDirection: TextDirection.ltr, child: Center( child: AspectRatio( - key: Key('same'), + key: const Key('same'), aspectRatio: controller.value.aspectRatio, child: VideoPlayer(controller), ), 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 373538ad365e..d20f47fd69ed 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 @@ -3,15 +3,38 @@ // found in the LICENSE file. 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'; import 'package:flutter/material.dart'; -import 'package:integration_test/integration_test.dart'; +import 'package:flutter/services.dart' show rootBundle; 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'; const Duration _playDuration = Duration(seconds: 1); +// Use WebM for web to allow CI to use Chromium. +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. + 'cb381ced070d356799dddf24aca38ce0579d3d7b' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); late VideoPlayerController _controller; @@ -19,7 +42,7 @@ void main() { group('asset videos', () { setUp(() { - _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); + _controller = VideoPlayerController.asset(_videoAssetKey); }); testWidgets('can be initialized', (WidgetTester tester) async { @@ -28,60 +51,17 @@ void main() { expect(_controller.value.isInitialized, true); expect(_controller.value.position, const Duration(seconds: 0)); expect(_controller.value.isPlaying, false); + // The WebM version has a slightly different duration than the MP4. expect(_controller.value.duration, - const Duration(seconds: 7, milliseconds: 540)); + const Duration(seconds: 7, milliseconds: kIsWeb ? 544 : 540)); }); - testWidgets( - 'reports buffering status', - (WidgetTester tester) async { - VideoPlayerController networkController = VideoPlayerController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', - ); - await networkController.initialize(); - // Mute to allow playing without DOM interaction on Web. - // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await networkController.setVolume(0); - final Completer started = Completer(); - final Completer ended = Completer(); - bool startedBuffering = false; - bool endedBuffering = false; - networkController.addListener(() { - if (networkController.value.isBuffering && !startedBuffering) { - startedBuffering = true; - started.complete(); - } - if (startedBuffering && - !networkController.value.isBuffering && - !endedBuffering) { - endedBuffering = true; - ended.complete(); - } - }); - - await networkController.play(); - await networkController.seekTo(const Duration(seconds: 5)); - await tester.pumpAndSettle(_playDuration); - await networkController.pause(); - - expect(networkController.value.isPlaying, false); - expect(networkController.value.position, - (Duration position) => position > const Duration(seconds: 0)); - - await started; - expect(startedBuffering, true); - - await ended; - expect(endedBuffering, true); - }, - skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), - ); - testWidgets( 'live stream duration != 0', (WidgetTester tester) async { - VideoPlayerController networkController = VideoPlayerController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + final VideoPlayerController networkController = + VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await networkController.initialize(); @@ -91,7 +71,7 @@ void main() { expect(networkController.value.duration, (Duration duration) => duration != Duration.zero); }, - skip: (kIsWeb), + skip: kIsWeb, ); testWidgets( @@ -150,7 +130,7 @@ void main() { // 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); - Duration tenMillisBeforeEnd = + final Duration tenMillisBeforeEnd = _controller.value.duration - const Duration(milliseconds: 10); await _controller.seekTo(tenMillisBeforeEnd); await _controller.play(); @@ -204,7 +184,7 @@ void main() { 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), @@ -225,4 +205,136 @@ void main() { // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 defaultTargetPlatform == TargetPlatform.iOS); }); + + group('file-based videos', () { + setUp(() async { + // Load the data from the asset. + final String tempDir = (await getTemporaryDirectory()).path; + final ByteData bytes = await rootBundle.load(_videoAssetKey); + + // Write it to a file to use as a source. + final String filename = _videoAssetKey.split('/').last; + final File file = File('$tempDir/$filename'); + await file.writeAsBytes(bytes.buffer.asInt8List()); + + _controller = VideoPlayerController.file(file); + }); + + testWidgets('test video player using static file() method as constructor', + (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + expect(_controller.value.isPlaying, true); + + await _controller.pause(); + expect(_controller.value.isPlaying, false); + }, skip: kIsWeb); + }); + + group('network videos', () { + setUp(() { + _controller = VideoPlayerController.network( + getUrlForAssetAsNetworkSource(_videoAssetKey)); + }); + + testWidgets( + 'reports buffering status', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + final Completer started = Completer(); + final Completer ended = Completer(); + _controller.addListener(() { + if (!started.isCompleted && _controller.value.isBuffering) { + started.complete(); + } + if (started.isCompleted && + !_controller.value.isBuffering && + !ended.isCompleted) { + ended.complete(); + } + }); + + await _controller.play(); + await _controller.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + + await expectLater(started.future, completes); + await expectLater(ended.future, completes); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); + }); + + // Audio playback is tested to prevent accidental regression, + // but could be removed in the future. + group('asset audios', () { + setUp(() { + _controller = VideoPlayerController.asset('assets/Audio.mp3'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + 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, + // 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, + const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41), + ); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, true); + expect( + _controller.value.position, + (Duration position) => position > const Duration(milliseconds: 0), + ); + }); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + await _controller.seekTo(const Duration(seconds: 3)); + + expect(_controller.value.position, const Duration(seconds: 3)); + }); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + 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); + }); + }); } diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile index 3924e59aa0f9..f7d6a5e68c3a 100644 --- a/packages/video_player/video_player/example/ios/Podfile +++ b/packages/video_player/video_player/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/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 2921ef9161be..2596398e0ff4 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,28 +15,8 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B0F5C77B94E32FB72444AE9F /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20721C28387E1F78689EC502 /* libPods-Runner.a */; }; - D182ECB59C06DBC7E2D5D913 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BD232FD3BD3343A5F52AF50 /* libPods-RunnerTests.a */; }; - F7151F2F26603EBD0028CB91 /* VideoPlayerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */; }; - F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - F7151F3126603EBD0028CB91 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; - F7151F3F26603ECA0028CB91 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -71,12 +51,6 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B15EC39F4617FE1082B18834 /* 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 = ""; }; C18C242FF01156F58C0DAF1C /* 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 = ""; }; - F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VideoPlayerUITests.m; sourceTree = ""; }; - F7151F3026603EBD0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VideoPlayerTests.m; sourceTree = ""; }; - F7151F3E26603ECA0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,21 +62,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7151F2926603EBD0028CB91 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F7151F3726603ECA0028CB91 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D182ECB59C06DBC7E2D5D913 /* libPods-RunnerTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -142,8 +101,6 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - F7151F3B26603ECA0028CB91 /* RunnerTests */, - F7151F2D26603EBD0028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 05E898481BC29A7FA83AA441 /* Pods */, 23104BB9DCF267F65AD246F9 /* Frameworks */, @@ -154,8 +111,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */, - F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -184,24 +139,6 @@ name = "Supporting Files"; sourceTree = ""; }; - F7151F2D26603EBD0028CB91 /* RunnerUITests */ = { - isa = PBXGroup; - children = ( - F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */, - F7151F3026603EBD0028CB91 /* Info.plist */, - ); - path = RunnerUITests; - sourceTree = ""; - }; - F7151F3B26603ECA0028CB91 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */, - F7151F3E26603ECA0028CB91 /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -226,65 +163,18 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - F7151F2B26603EBD0028CB91 /* RunnerUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F7151F3526603EBD0028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; - buildPhases = ( - F7151F2826603EBD0028CB91 /* Sources */, - F7151F2926603EBD0028CB91 /* Frameworks */, - F7151F2A26603EBD0028CB91 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F7151F3226603EBD0028CB91 /* PBXTargetDependency */, - ); - name = RunnerUITests; - productName = RunnerUITests; - productReference = F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; - F7151F3926603ECA0028CB91 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F7151F4126603ECB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - E9F7B01F913C69934A6629F6 /* [CP] Check Pods Manifest.lock */, - F7151F3626603ECA0028CB91 /* Sources */, - F7151F3726603ECA0028CB91 /* Frameworks */, - F7151F3826603ECA0028CB91 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F7151F4026603ECA0028CB91 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; - F7151F2B26603EBD0028CB91 = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - F7151F3926603ECA0028CB91 = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -301,8 +191,6 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - F7151F3926603ECA0028CB91 /* RunnerTests */, - F7151F2B26603EBD0028CB91 /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -319,20 +207,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7151F2A26603EBD0028CB91 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F7151F3826603ECA0028CB91 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -364,28 +238,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - E9F7B01F913C69934A6629F6 /* [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; - }; F31A669BD45D5A7C940BF077 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -417,37 +269,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7151F2826603EBD0028CB91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7151F2F26603EBD0028CB91 /* VideoPlayerUITests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F7151F3626603ECA0028CB91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - F7151F3226603EBD0028CB91 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F7151F3126603EBD0028CB91 /* PBXContainerItemProxy */; - }; - F7151F4026603ECA0028CB91 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F7151F3F26603ECA0028CB91 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -616,62 +439,6 @@ }; name = Release; }; - F7151F3326603EBD0028CB91 /* Debug */ = { - isa = XCBuildConfiguration; - 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; - }; - F7151F3426603EBD0028CB91 /* Release */ = { - isa = XCBuildConfiguration; - 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; - }; - F7151F4226603ECB0028CB91 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6CDC4DA5940705A6E7671616 /* 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"; - 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; - }; - F7151F4326603ECB0028CB91 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2A2EA522BDC492279A91AB75 /* 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"; - 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; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -693,24 +460,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F7151F3526603EBD0028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7151F3326603EBD0028CB91 /* Debug */, - F7151F3426603EBD0028CB91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F7151F4126603ECB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7151F4226603ECB0028CB91 /* Debug */, - F7151F4326603ECB0028CB91 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3f1ee9541e2f..0632b6533bc8 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m deleted file mode 100644 index 890866f34952..000000000000 --- a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.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 video_player; -@import XCTest; - -@interface VideoPlayerTests : XCTestCase -@end - -@implementation VideoPlayerTests - -- (void)testPlugin { - FLTVideoPlayerPlugin* plugin = [[FLTVideoPlayerPlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end diff --git a/packages/video_player/video_player/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player/example/ios/RunnerUITests/VideoPlayerUITests.m deleted file mode 100644 index 62d8c532ca03..000000000000 --- a/packages/video_player/video_player/example/ios/RunnerUITests/VideoPlayerUITests.m +++ /dev/null @@ -1,52 +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 os.log; -@import XCTest; - -@interface VideoPlayerUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; -@end - -@implementation VideoPlayerUITests - -- (void)setUp { - self.continueAfterFailure = NO; - - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testTabs { - XCUIApplication* app = self.app; - - XCUIElement* remoteTab = [app.otherElements - elementMatchingPredicate:[NSPredicate predicateWithFormat:@"selected == YES"]]; - if (![remoteTab waitForExistenceWithTimeout:30.0]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find selected Remote tab"); - } - XCTAssertTrue([remoteTab.label containsString:@"Remote"]); - - for (NSString* tabName in @[ @"Asset", @"List example" ]) { - NSPredicate* predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]; - XCUIElement* unselectedTab = [app.staticTexts elementMatchingPredicate:predicate]; - if (![unselectedTab waitForExistenceWithTimeout:30.0]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find unselected %@ tab", tabName); - } - XCTAssertFalse(unselectedTab.isSelected); - [unselectedTab tap]; - - XCUIElement* selectedTab = [app.otherElements - elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]]; - if (![selectedTab waitForExistenceWithTimeout:30.0]) { - os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find selected %@ tab", tabName); - } - XCTAssertTrue(selectedTab.isSelected); - } -} - -@end diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index eef23197ef50..63afc4a28bc8 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'; @@ -47,10 +46,10 @@ class _App extends StatelessWidget { tabs: [ Tab( icon: Icon(Icons.cloud), - text: "Remote", + text: 'Remote', ), - Tab(icon: Icon(Icons.insert_drive_file), text: "Asset"), - Tab(icon: Icon(Icons.list), text: "List example"), + Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), + Tab(icon: Icon(Icons.list), text: 'List example'), ], ), ), @@ -71,20 +70,20 @@ class _ButterFlyAssetVideoInList extends StatelessWidget { Widget build(BuildContext context) { return ListView( children: [ - _ExampleCard(title: "Item a"), - _ExampleCard(title: "Item b"), - _ExampleCard(title: "Item c"), - _ExampleCard(title: "Item d"), - _ExampleCard(title: "Item e"), - _ExampleCard(title: "Item f"), - _ExampleCard(title: "Item g"), + const _ExampleCard(title: 'Item a'), + const _ExampleCard(title: 'Item b'), + const _ExampleCard(title: 'Item c'), + const _ExampleCard(title: 'Item d'), + const _ExampleCard(title: 'Item e'), + const _ExampleCard(title: 'Item f'), + const _ExampleCard(title: 'Item g'), Card( child: Column(children: [ Column( children: [ const ListTile( leading: Icon(Icons.cake), - title: Text("Video video"), + title: Text('Video video'), ), Stack( alignment: FractionalOffset.bottomRight + @@ -96,11 +95,11 @@ class _ButterFlyAssetVideoInList extends StatelessWidget { ], ), ])), - _ExampleCard(title: "Item h"), - _ExampleCard(title: "Item i"), - _ExampleCard(title: "Item j"), - _ExampleCard(title: "Item k"), - _ExampleCard(title: "Item l"), + const _ExampleCard(title: 'Item h'), + const _ExampleCard(title: 'Item i'), + const _ExampleCard(title: 'Item j'), + const _ExampleCard(title: 'Item k'), + const _ExampleCard(title: 'Item l'), ], ); } @@ -210,8 +209,9 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { Future _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context) - .loadString('assets/bumble_bee_captions.srt'); - return SubRipCaptionFile(fileContents); + .loadString('assets/bumble_bee_captions.vtt'); + return WebVTTCaptionFile( + fileContents); // For vtt files, use WebVTTCaptionFile } @override @@ -268,7 +268,18 @@ class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({Key? key, required this.controller}) : super(key: key); - static const _examplePlaybackRates = [ + static const List _exampleCaptionOffsets = [ + Duration(seconds: -10), + Duration(seconds: -3), + Duration(seconds: -1, milliseconds: -500), + Duration(milliseconds: -250), + Duration(milliseconds: 0), + Duration(milliseconds: 250), + Duration(seconds: 1, milliseconds: 500), + Duration(seconds: 3), + Duration(seconds: 10), + ]; + static const List _examplePlaybackRates = [ 0.25, 0.5, 1.0, @@ -286,17 +297,18 @@ class _ControlsOverlay extends StatelessWidget { return Stack( children: [ AnimatedSwitcher( - duration: Duration(milliseconds: 50), - reverseDuration: Duration(milliseconds: 200), + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), child: controller.value.isPlaying - ? SizedBox.shrink() + ? const SizedBox.shrink() : Container( color: Colors.black26, - child: Center( + child: const Center( child: Icon( Icons.play_arrow, color: Colors.white, size: 100.0, + semanticLabel: 'Play', ), ), ), @@ -306,18 +318,47 @@ class _ControlsOverlay extends StatelessWidget { controller.value.isPlaying ? controller.pause() : controller.play(); }, ), + Align( + alignment: Alignment.topLeft, + child: PopupMenuButton( + initialValue: controller.value.captionOffset, + tooltip: 'Caption Offset', + onSelected: (Duration delay) { + controller.setCaptionOffset(delay); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final Duration offsetDuration in _exampleCaptionOffsets) + PopupMenuItem( + value: offsetDuration, + child: Text('${offsetDuration.inMilliseconds}ms'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.captionOffset.inMilliseconds}ms'), + ), + ), + ), Align( alignment: Alignment.topRight, child: PopupMenuButton( initialValue: controller.value.playbackSpeed, tooltip: 'Playback speed', - onSelected: (speed) { + onSelected: (double speed) { controller.setPlaybackSpeed(speed); }, - itemBuilder: (context) { - return [ - for (final speed in _examplePlaybackRates) - PopupMenuItem( + itemBuilder: (BuildContext context) { + return >[ + for (final double speed in _examplePlaybackRates) + PopupMenuItem( value: speed, child: Text('${speed}x'), ) @@ -383,7 +424,7 @@ class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> { 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 63f179a06211..e6b51f99e115 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,18 +18,21 @@ dependencies: path: ../ dev_dependencies: - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter + path_provider: ^2.0.6 test: any - pedantic: ^1.10.0 flutter: uses-material-design: true assets: - - assets/flutter-mark-square-64.png - - assets/Butterfly-209.mp4 - - assets/bumble_bee_captions.srt + - assets/flutter-mark-square-64.png + - assets/Butterfly-209.mp4 + - assets/Butterfly-209.webm + - assets/bumble_bee_captions.srt + - assets/bumble_bee_captions.vtt + - assets/Audio.mp3 diff --git a/packages/video_player/video_player/example/test_driver/video_player_test.dart b/packages/video_player/video_player/example/test_driver/video_player_test.dart index 1d5ac79c77bf..5fbed804d8d8 100644 --- a/packages/video_player/video_player/example/test_driver/video_player_test.dart +++ b/packages/video_player/video_player/example/test_driver/video_player_test.dart @@ -12,8 +12,8 @@ Future main() async { await driver.close(); }); - //TODO(cyanglaz): Use TabBar tabs to navigate between pages after https://github.com/flutter/flutter/issues/16991 is fixed. - //TODO(cyanglaz): Un-skip the test after https://github.com/flutter/flutter/issues/43012 is fixed + // TODO(cyanglaz): Use TabBar tabs to navigate between pages after https://github.com/flutter/flutter/issues/16991 is fixed. + // TODO(cyanglaz): Un-skip the test after https://github.com/flutter/flutter/issues/43012 is fixed test('Push a page contains video and pop back, do not crash.', () async { final SerializableFinder pushTab = find.byValueKey('push_tab'); await driver.waitFor(pushTab); diff --git a/packages/video_player/video_player/example/video_player_example.iml b/packages/video_player/video_player/example/video_player_example.iml deleted file mode 100644 index dafb001137cd..000000000000 --- a/packages/video_player/video_player/example/video_player_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/video_player/video_player/example/video_player_example_android.iml b/packages/video_player/video_player/example/video_player_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/video_player/example/video_player_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/video_player/video_player/ios/Assets/.gitkeep b/packages/video_player/video_player/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m deleted file mode 100644 index f0f672d87431..000000000000 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ /dev/null @@ -1,618 +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 "FLTVideoPlayerPlugin.h" -#import -#import -#import "messages.h" - -#if !__has_feature(objc_arc) -#error Code Requires ARC. -#endif - -@interface FLTFrameUpdater : NSObject -@property(nonatomic) int64_t textureId; -@property(nonatomic, weak, readonly) NSObject* registry; -- (void)onDisplayLink:(CADisplayLink*)link; -@end - -@implementation FLTFrameUpdater -- (FLTFrameUpdater*)initWithRegistry:(NSObject*)registry { - NSAssert(self, @"super init cannot be nil"); - if (self == nil) return nil; - _registry = registry; - return self; -} - -- (void)onDisplayLink:(CADisplayLink*)link { - [_registry textureFrameAvailable:_textureId]; -} -@end - -@interface FLTVideoPlayer : NSObject -@property(readonly, nonatomic) AVPlayer* player; -@property(readonly, nonatomic) AVPlayerItemVideoOutput* videoOutput; -@property(readonly, nonatomic) CADisplayLink* displayLink; -@property(nonatomic) FlutterEventChannel* eventChannel; -@property(nonatomic) FlutterEventSink eventSink; -@property(nonatomic) CGAffineTransform preferredTransform; -@property(nonatomic, readonly) bool disposed; -@property(nonatomic, readonly) bool isPlaying; -@property(nonatomic) bool isLooping; -@property(nonatomic, readonly) bool isInitialized; -- (instancetype)initWithURL:(NSURL*)url - frameUpdater:(FLTFrameUpdater*)frameUpdater - httpHeaders:(NSDictionary*)headers; -- (void)play; -- (void)pause; -- (void)setIsLooping:(bool)isLooping; -- (void)updatePlayingState; -@end - -static void* timeRangeContext = &timeRangeContext; -static void* statusContext = &statusContext; -static void* playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; -static void* playbackBufferEmptyContext = &playbackBufferEmptyContext; -static void* playbackBufferFullContext = &playbackBufferFullContext; - -@implementation FLTVideoPlayer -- (instancetype)initWithAsset:(NSString*)asset frameUpdater:(FLTFrameUpdater*)frameUpdater { - NSString* path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; - return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:nil]; -} - -- (void)addObservers:(AVPlayerItem*)item { - [item addObserver:self - forKeyPath:@"loadedTimeRanges" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:timeRangeContext]; - [item addObserver:self - forKeyPath:@"status" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:statusContext]; - [item addObserver:self - forKeyPath:@"playbackLikelyToKeepUp" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:playbackLikelyToKeepUpContext]; - [item addObserver:self - forKeyPath:@"playbackBufferEmpty" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:playbackBufferEmptyContext]; - [item addObserver:self - forKeyPath:@"playbackBufferFull" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:playbackBufferFullContext]; - - // Add an observer that will respond to itemDidPlayToEndTime - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(itemDidPlayToEndTime:) - name:AVPlayerItemDidPlayToEndTimeNotification - object:item]; -} - -- (void)itemDidPlayToEndTime:(NSNotification*)notification { - if (_isLooping) { - AVPlayerItem* p = [notification object]; - [p seekToTime:kCMTimeZero completionHandler:nil]; - } else { - if (_eventSink) { - _eventSink(@{@"event" : @"completed"}); - } - } -} - -const int64_t TIME_UNSET = -9223372036854775807; - -static inline int64_t FLTCMTimeToMillis(CMTime time) { - // When CMTIME_IS_INDEFINITE return a value that matches TIME_UNSET from ExoPlayer2 on Android. - // Fixes https://github.com/flutter/flutter/issues/48670 - if (CMTIME_IS_INDEFINITE(time)) return TIME_UNSET; - if (time.timescale == 0) return 0; - return time.value * 1000 / time.timescale; -} - -static inline CGFloat radiansToDegrees(CGFloat radians) { - // Input range [-pi, pi] or [-180, 180] - CGFloat degrees = GLKMathRadiansToDegrees((float)radians); - if (degrees < 0) { - // Convert -90 to 270 and -180 to 180 - return degrees + 360; - } - // Output degrees in between [0, 360[ - return degrees; -}; - -- (AVMutableVideoComposition*)getVideoCompositionWithTransform:(CGAffineTransform)transform - withAsset:(AVAsset*)asset - withVideoTrack:(AVAssetTrack*)videoTrack { - AVMutableVideoCompositionInstruction* instruction = - [AVMutableVideoCompositionInstruction videoCompositionInstruction]; - instruction.timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]); - AVMutableVideoCompositionLayerInstruction* layerInstruction = - [AVMutableVideoCompositionLayerInstruction - videoCompositionLayerInstructionWithAssetTrack:videoTrack]; - [layerInstruction setTransform:_preferredTransform atTime:kCMTimeZero]; - - AVMutableVideoComposition* videoComposition = [AVMutableVideoComposition videoComposition]; - instruction.layerInstructions = @[ layerInstruction ]; - videoComposition.instructions = @[ instruction ]; - - // If in portrait mode, switch the width and height of the video - CGFloat width = videoTrack.naturalSize.width; - CGFloat height = videoTrack.naturalSize.height; - NSInteger rotationDegrees = - (NSInteger)round(radiansToDegrees(atan2(_preferredTransform.b, _preferredTransform.a))); - if (rotationDegrees == 90 || rotationDegrees == 270) { - width = videoTrack.naturalSize.height; - height = videoTrack.naturalSize.width; - } - videoComposition.renderSize = CGSizeMake(width, height); - - // TODO(@recastrodiaz): should we use videoTrack.nominalFrameRate ? - // Currently set at a constant 30 FPS - videoComposition.frameDuration = CMTimeMake(1, 30); - - return videoComposition; -} - -- (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater*)frameUpdater { - NSDictionary* pixBuffAttributes = @{ - (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), - (id)kCVPixelBufferIOSurfacePropertiesKey : @{} - }; - _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes]; - - _displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater - selector:@selector(onDisplayLink:)]; - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - _displayLink.paused = YES; -} - -- (instancetype)initWithURL:(NSURL*)url - frameUpdater:(FLTFrameUpdater*)frameUpdater - httpHeaders:(NSDictionary*)headers { - NSDictionary* options = nil; - if (headers != nil && [headers count] != 0) { - options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers}; - } - AVURLAsset* urlAsset = [AVURLAsset URLAssetWithURL:url options:options]; - AVPlayerItem* item = [AVPlayerItem playerItemWithAsset:urlAsset]; - 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]; - NSAssert(self, @"super init cannot be nil"); - _isInitialized = false; - _isPlaying = false; - _disposed = false; - - AVAsset* asset = [item asset]; - void (^assetCompletionHandler)(void) = ^{ - if ([asset statusOfValueForKey:@"tracks" error:nil] == AVKeyValueStatusLoaded) { - NSArray* tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; - if ([tracks count] > 0) { - AVAssetTrack* videoTrack = tracks[0]; - void (^trackCompletionHandler)(void) = ^{ - if (self->_disposed) return; - if ([videoTrack statusOfValueForKey:@"preferredTransform" - error:nil] == AVKeyValueStatusLoaded) { - // Rotate the video by using a videoComposition and the preferredTransform - self->_preferredTransform = [self fixTransform: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 - // use with media served using HTTP Live Streaming. - AVMutableVideoComposition* videoComposition = - [self getVideoCompositionWithTransform:self->_preferredTransform - withAsset:asset - withVideoTrack:videoTrack]; - item.videoComposition = videoComposition; - } - }; - [videoTrack loadValuesAsynchronouslyForKeys:@[ @"preferredTransform" ] - completionHandler:trackCompletionHandler]; - } - } - }; - - _player = [AVPlayer playerWithPlayerItem:item]; - _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - - [self createVideoOutputAndDisplayLink:frameUpdater]; - - [self addObservers:item]; - - [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler]; - - return self; -} - -- (void)observeValueForKeyPath:(NSString*)path - ofObject:(id)object - change:(NSDictionary*)change - context:(void*)context { - if (context == timeRangeContext) { - if (_eventSink != nil) { - NSMutableArray*>* values = [[NSMutableArray alloc] init]; - for (NSValue* rangeValue in [object loadedTimeRanges]) { - CMTimeRange range = [rangeValue CMTimeRangeValue]; - int64_t start = FLTCMTimeToMillis(range.start); - [values addObject:@[ @(start), @(start + FLTCMTimeToMillis(range.duration)) ]]; - } - _eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); - } - } else if (context == statusContext) { - AVPlayerItem* item = (AVPlayerItem*)object; - switch (item.status) { - case AVPlayerItemStatusFailed: - if (_eventSink != nil) { - _eventSink([FlutterError - errorWithCode:@"VideoError" - message:[@"Failed to load video: " - stringByAppendingString:[item.error localizedDescription]] - details:nil]); - } - break; - case AVPlayerItemStatusUnknown: - break; - case AVPlayerItemStatusReadyToPlay: - [item addOutput:_videoOutput]; - [self sendInitialized]; - [self updatePlayingState]; - break; - } - } else if (context == playbackLikelyToKeepUpContext) { - if ([[_player currentItem] isPlaybackLikelyToKeepUp]) { - [self updatePlayingState]; - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingEnd"}); - } - } - } else if (context == playbackBufferEmptyContext) { - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingStart"}); - } - } else if (context == playbackBufferFullContext) { - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingEnd"}); - } - } -} - -- (void)updatePlayingState { - if (!_isInitialized) { - return; - } - if (_isPlaying) { - [_player play]; - } else { - [_player pause]; - } - _displayLink.paused = !_isPlaying; -} - -- (void)sendInitialized { - if (_eventSink && !_isInitialized) { - CGSize size = [self.player currentItem].presentationSize; - CGFloat width = size.width; - CGFloat height = size.height; - - // The player has not yet initialized. - if (height == CGSizeZero.height && width == CGSizeZero.width) { - return; - } - // The player may be initialized but still needs to determine the duration. - if ([self duration] == 0) { - return; - } - - _isInitialized = true; - _eventSink(@{ - @"event" : @"initialized", - @"duration" : @([self duration]), - @"width" : @(width), - @"height" : @(height) - }); - } -} - -- (void)play { - _isPlaying = true; - [self updatePlayingState]; -} - -- (void)pause { - _isPlaying = false; - [self updatePlayingState]; -} - -- (int64_t)position { - return FLTCMTimeToMillis([_player currentTime]); -} - -- (int64_t)duration { - return FLTCMTimeToMillis([[_player currentItem] duration]); -} - -- (void)seekTo:(int)location { - [_player seekToTime:CMTimeMake(location, 1000) - toleranceBefore:kCMTimeZero - toleranceAfter:kCMTimeZero]; -} - -- (void)setIsLooping:(bool)isLooping { - _isLooping = isLooping; -} - -- (void)setVolume:(double)volume { - _player.volume = (float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume)); -} - -- (void)setPlaybackSpeed:(double)speed { - // See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of - // these checks. - if (speed > 2.0 && !_player.currentItem.canPlayFastForward) { - if (_eventSink != nil) { - _eventSink([FlutterError errorWithCode:@"VideoError" - message:@"Video cannot be fast-forwarded beyond 2.0x" - details:nil]); - } - return; - } - - if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) { - if (_eventSink != nil) { - _eventSink([FlutterError errorWithCode:@"VideoError" - message:@"Video cannot be slow-forwarded" - details:nil]); - } - return; - } - - _player.rate = speed; -} - -- (CVPixelBufferRef)copyPixelBuffer { - CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; - if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { - return [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; - } else { - return NULL; - } -} - -- (void)onTextureUnregistered:(NSObject*)texture { - dispatch_async(dispatch_get_main_queue(), ^{ - [self dispose]; - }); -} - -- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments { - _eventSink = nil; - return nil; -} - -- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments - eventSink:(nonnull FlutterEventSink)events { - _eventSink = events; - // TODO(@recastrodiaz): remove the line below when the race condition is resolved: - // https://github.com/flutter/flutter/issues/21483 - // This line ensures the 'initialized' event is sent when the event - // 'AVPlayerItemStatusReadyToPlay' fires before _eventSink is set (this function - // onListenWithArguments is called) - [self sendInitialized]; - return nil; -} - -/// This method allows you to dispose without touching the event channel. This -/// is useful for the case where the Engine is in the process of deconstruction -/// so the channel is going to die or is already dead. -- (void)disposeSansEventChannel { - _disposed = true; - [_displayLink invalidate]; - [[_player currentItem] removeObserver:self forKeyPath:@"status" context:statusContext]; - [[_player currentItem] removeObserver:self - forKeyPath:@"loadedTimeRanges" - context:timeRangeContext]; - [[_player currentItem] removeObserver:self - forKeyPath:@"playbackLikelyToKeepUp" - context:playbackLikelyToKeepUpContext]; - [[_player currentItem] removeObserver:self - forKeyPath:@"playbackBufferEmpty" - context:playbackBufferEmptyContext]; - [[_player currentItem] removeObserver:self - forKeyPath:@"playbackBufferFull" - context:playbackBufferFullContext]; - [_player replaceCurrentItemWithPlayerItem:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)dispose { - [self disposeSansEventChannel]; - [_eventChannel setStreamHandler:nil]; -} - -@end - -@interface FLTVideoPlayerPlugin () -@property(readonly, weak, nonatomic) NSObject* registry; -@property(readonly, weak, nonatomic) NSObject* messenger; -@property(readonly, strong, nonatomic) NSMutableDictionary* players; -@property(readonly, strong, nonatomic) NSObject* registrar; -@end - -@implementation FLTVideoPlayerPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTVideoPlayerPlugin* instance = [[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; - [registrar publish:instance]; - FLTVideoPlayerApiSetup(registrar.messenger, instance); -} - -- (instancetype)initWithRegistrar:(NSObject*)registrar { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _registry = [registrar textures]; - _messenger = [registrar messenger]; - _registrar = registrar; - _players = [NSMutableDictionary dictionaryWithCapacity:1]; - return self; -} - -- (void)detachFromEngineForRegistrar:(NSObject*)registrar { - for (NSNumber* textureId in _players.allKeys) { - FLTVideoPlayer* player = _players[textureId]; - [player disposeSansEventChannel]; - } - [_players removeAllObjects]; - // 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); -} - -- (FLTTextureMessage*)onPlayerSetup:(FLTVideoPlayer*)player - frameUpdater:(FLTFrameUpdater*)frameUpdater { - int64_t textureId = [_registry registerTexture:player]; - frameUpdater.textureId = textureId; - FlutterEventChannel* eventChannel = [FlutterEventChannel - eventChannelWithName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%lld", - textureId] - binaryMessenger:_messenger]; - [eventChannel setStreamHandler:player]; - player.eventChannel = eventChannel; - _players[@(textureId)] = player; - FLTTextureMessage* result = [[FLTTextureMessage alloc] init]; - result.textureId = @(textureId); - return result; -} - -- (void)initialize:(FlutterError* __autoreleasing*)error { - // Allow audio playback when the Ring/Silent switch is set to silent - [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; - - for (NSNumber* textureId in _players) { - [_registry unregisterTexture:[textureId unsignedIntegerValue]]; - [_players[textureId] dispose]; - } - [_players removeAllObjects]; -} - -- (FLTTextureMessage*)create:(FLTCreateMessage*)input error:(FlutterError**)error { - FLTFrameUpdater* frameUpdater = [[FLTFrameUpdater alloc] initWithRegistry:_registry]; - FLTVideoPlayer* player; - if (input.asset) { - NSString* assetPath; - if (input.packageName) { - assetPath = [_registrar lookupKeyForAsset:input.asset fromPackage:input.packageName]; - } else { - assetPath = [_registrar lookupKeyForAsset:input.asset]; - } - player = [[FLTVideoPlayer alloc] initWithAsset:assetPath frameUpdater:frameUpdater]; - return [self onPlayerSetup:player frameUpdater:frameUpdater]; - } else if (input.uri) { - player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri] - frameUpdater:frameUpdater - httpHeaders:input.httpHeaders]; - return [self onPlayerSetup:player frameUpdater:frameUpdater]; - } else { - *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; - return nil; - } -} - -- (void)dispose:(FLTTextureMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [_registry unregisterTexture:input.textureId.intValue]; - [_players removeObjectForKey:input.textureId]; - // If the Flutter contains https://github.com/flutter/engine/pull/12695, - // the `player` is disposed via `onTextureUnregistered` at the right time. - // Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the - // texture has completed the un-reregistration. It may leads a crash if we dispose the - // `player` before the texture is unregistered. We add a dispatch_after hack to make sure the - // texture is unregistered before we dispose the `player`. - // - // TODO(cyanglaz): Remove this dispatch block when - // https://github.com/flutter/flutter/commit/8159a9906095efc9af8b223f5e232cb63542ad0b is in - // stable And update the min flutter version of the plugin to the stable version. - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), - dispatch_get_main_queue(), ^{ - if (!player.disposed) { - [player dispose]; - } - }); -} - -- (void)setLooping:(FLTLoopingMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [player setIsLooping:[input.isLooping boolValue]]; -} - -- (void)setVolume:(FLTVolumeMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [player setVolume:[input.volume doubleValue]]; -} - -- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [player setPlaybackSpeed:[input.speed doubleValue]]; -} - -- (void)play:(FLTTextureMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [player play]; -} - -- (FLTPositionMessage*)position:(FLTTextureMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - FLTPositionMessage* result = [[FLTPositionMessage alloc] init]; - result.position = @([player position]); - return result; -} - -- (void)seekTo:(FLTPositionMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [player seekTo:[input.position intValue]]; -} - -- (void)pause:(FLTTextureMessage*)input error:(FlutterError**)error { - FLTVideoPlayer* player = _players[input.textureId]; - [player pause]; -} - -- (void)setMixWithOthers:(FLTMixWithOthersMessage*)input - error:(FlutterError* _Nullable __autoreleasing*)error { - if ([input.mixWithOthers boolValue]) { - [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback - withOptions:AVAudioSessionCategoryOptionMixWithOthers - error:nil]; - } else { - [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; - } -} - -@end diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h deleted file mode 100644 index e21e7860ba09..000000000000 --- a/packages/video_player/video_player/ios/Classes/messages.h +++ /dev/null @@ -1,79 +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 -#import -@protocol FlutterBinaryMessenger; -@class FlutterError; -@class FlutterStandardTypedData; - -NS_ASSUME_NONNULL_BEGIN - -@class FLTTextureMessage; -@class FLTCreateMessage; -@class FLTLoopingMessage; -@class FLTVolumeMessage; -@class FLTPlaybackSpeedMessage; -@class FLTPositionMessage; -@class FLTMixWithOthersMessage; - -@interface FLTTextureMessage : NSObject -@property(nonatomic, strong, nullable) NSNumber *textureId; -@end - -@interface FLTCreateMessage : NSObject -@property(nonatomic, copy, nullable) NSString *asset; -@property(nonatomic, copy, nullable) NSString *uri; -@property(nonatomic, copy, nullable) NSString *packageName; -@property(nonatomic, copy, nullable) NSString *formatHint; -@property(nonatomic, strong, nullable) NSDictionary *httpHeaders; -@end - -@interface FLTLoopingMessage : NSObject -@property(nonatomic, strong, nullable) NSNumber *textureId; -@property(nonatomic, strong, nullable) NSNumber *isLooping; -@end - -@interface FLTVolumeMessage : NSObject -@property(nonatomic, strong, nullable) NSNumber *textureId; -@property(nonatomic, strong, nullable) NSNumber *volume; -@end - -@interface FLTPlaybackSpeedMessage : NSObject -@property(nonatomic, strong, nullable) NSNumber *textureId; -@property(nonatomic, strong, nullable) NSNumber *speed; -@end - -@interface FLTPositionMessage : NSObject -@property(nonatomic, strong, nullable) NSNumber *textureId; -@property(nonatomic, strong, nullable) NSNumber *position; -@end - -@interface FLTMixWithOthersMessage : NSObject -@property(nonatomic, strong, nullable) NSNumber *mixWithOthers; -@end - -@protocol FLTVideoPlayerApi -- (void)initialize:(FlutterError *_Nullable *_Nonnull)error; -- (nullable FLTTextureMessage *)create:(FLTCreateMessage *)input - error:(FlutterError *_Nullable *_Nonnull)error; -- (void)dispose:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setLooping:(FLTLoopingMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setVolume:(FLTVolumeMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage *)input - error:(FlutterError *_Nullable *_Nonnull)error; -- (void)play:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable FLTPositionMessage *)position:(FLTTextureMessage *)input - error:(FlutterError *_Nullable *_Nonnull)error; -- (void)seekTo:(FLTPositionMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; -- (void)pause:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setMixWithOthers:(FLTMixWithOthersMessage *)input - error:(FlutterError *_Nullable *_Nonnull)error; -@end - -extern void FLTVideoPlayerApiSetup(id binaryMessenger, - id _Nullable api); - -NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player/ios/Classes/messages.m b/packages/video_player/video_player/ios/Classes/messages.m deleted file mode 100644 index 0936bbc7d995..000000000000 --- a/packages/video_player/video_player/ios/Classes/messages.m +++ /dev/null @@ -1,379 +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 -#import "messages.h" -#import - -#if !__has_feature(objc_arc) -#error File requires ARC to be enabled. -#endif - -static NSDictionary *wrapResult(NSDictionary *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, - }; -} - -@interface FLTTextureMessage () -+ (FLTTextureMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end -@interface FLTCreateMessage () -+ (FLTCreateMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end -@interface FLTLoopingMessage () -+ (FLTLoopingMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end -@interface FLTVolumeMessage () -+ (FLTVolumeMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end -@interface FLTPlaybackSpeedMessage () -+ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end -@interface FLTPositionMessage () -+ (FLTPositionMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end -@interface FLTMixWithOthersMessage () -+ (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; -@end - -@implementation FLTTextureMessage -+ (FLTTextureMessage *)fromMap:(NSDictionary *)dict { - FLTTextureMessage *result = [[FLTTextureMessage alloc] init]; - result.textureId = dict[@"textureId"]; - if ((NSNull *)result.textureId == [NSNull null]) { - result.textureId = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", nil]; -} -@end - -@implementation FLTCreateMessage -+ (FLTCreateMessage *)fromMap:(NSDictionary *)dict { - FLTCreateMessage *result = [[FLTCreateMessage alloc] init]; - result.asset = dict[@"asset"]; - if ((NSNull *)result.asset == [NSNull null]) { - result.asset = nil; - } - result.uri = dict[@"uri"]; - if ((NSNull *)result.uri == [NSNull null]) { - result.uri = nil; - } - result.packageName = dict[@"packageName"]; - if ((NSNull *)result.packageName == [NSNull null]) { - result.packageName = nil; - } - result.formatHint = dict[@"formatHint"]; - if ((NSNull *)result.formatHint == [NSNull null]) { - result.formatHint = nil; - } - result.httpHeaders = dict[@"httpHeaders"]; - if ((NSNull *)result.httpHeaders == [NSNull null]) { - result.httpHeaders = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.asset ? self.asset : [NSNull null]), @"asset", - (self.uri ? self.uri : [NSNull null]), @"uri", - (self.packageName ? self.packageName : [NSNull null]), - @"packageName", - (self.formatHint ? self.formatHint : [NSNull null]), - @"formatHint", - (self.httpHeaders ? self.httpHeaders : [NSNull null]), - @"httpHeaders", nil]; -} -@end - -@implementation FLTLoopingMessage -+ (FLTLoopingMessage *)fromMap:(NSDictionary *)dict { - FLTLoopingMessage *result = [[FLTLoopingMessage alloc] init]; - result.textureId = dict[@"textureId"]; - if ((NSNull *)result.textureId == [NSNull null]) { - result.textureId = nil; - } - result.isLooping = dict[@"isLooping"]; - if ((NSNull *)result.isLooping == [NSNull null]) { - result.isLooping = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", - (self.isLooping != nil ? self.isLooping : [NSNull null]), - @"isLooping", nil]; -} -@end - -@implementation FLTVolumeMessage -+ (FLTVolumeMessage *)fromMap:(NSDictionary *)dict { - FLTVolumeMessage *result = [[FLTVolumeMessage alloc] init]; - result.textureId = dict[@"textureId"]; - if ((NSNull *)result.textureId == [NSNull null]) { - result.textureId = nil; - } - result.volume = dict[@"volume"]; - if ((NSNull *)result.volume == [NSNull null]) { - result.volume = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", (self.volume != nil ? self.volume : [NSNull null]), - @"volume", nil]; -} -@end - -@implementation FLTPlaybackSpeedMessage -+ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict { - FLTPlaybackSpeedMessage *result = [[FLTPlaybackSpeedMessage alloc] init]; - result.textureId = dict[@"textureId"]; - if ((NSNull *)result.textureId == [NSNull null]) { - result.textureId = nil; - } - result.speed = dict[@"speed"]; - if ((NSNull *)result.speed == [NSNull null]) { - result.speed = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", (self.speed != nil ? self.speed : [NSNull null]), - @"speed", nil]; -} -@end - -@implementation FLTPositionMessage -+ (FLTPositionMessage *)fromMap:(NSDictionary *)dict { - FLTPositionMessage *result = [[FLTPositionMessage alloc] init]; - result.textureId = dict[@"textureId"]; - if ((NSNull *)result.textureId == [NSNull null]) { - result.textureId = nil; - } - result.position = dict[@"position"]; - if ((NSNull *)result.position == [NSNull null]) { - result.position = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", - (self.position != nil ? self.position : [NSNull null]), - @"position", nil]; -} -@end - -@implementation FLTMixWithOthersMessage -+ (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict { - FLTMixWithOthersMessage *result = [[FLTMixWithOthersMessage alloc] init]; - result.mixWithOthers = dict[@"mixWithOthers"]; - if ((NSNull *)result.mixWithOthers == [NSNull null]) { - result.mixWithOthers = nil; - } - return result; -} -- (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.mixWithOthers != nil ? self.mixWithOthers : [NSNull null]), - @"mixWithOthers", nil]; -} -@end - -void FLTVideoPlayerApiSetup(id binaryMessenger, id api) { - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.initialize" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - [api initialize:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.create" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTCreateMessage *input = [FLTCreateMessage fromMap:message]; - FlutterError *error; - FLTTextureMessage *output = [api create:input error:&error]; - callback(wrapResult([output toMap], error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.dispose" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTTextureMessage *input = [FLTTextureMessage fromMap:message]; - FlutterError *error; - [api dispose:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setLooping" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTLoopingMessage *input = [FLTLoopingMessage fromMap:message]; - FlutterError *error; - [api setLooping:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setVolume" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTVolumeMessage *input = [FLTVolumeMessage fromMap:message]; - FlutterError *error; - [api setVolume:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTPlaybackSpeedMessage *input = [FLTPlaybackSpeedMessage fromMap:message]; - FlutterError *error; - [api setPlaybackSpeed:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.play" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTTextureMessage *input = [FLTTextureMessage fromMap:message]; - FlutterError *error; - [api play:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.position" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTTextureMessage *input = [FLTTextureMessage fromMap:message]; - FlutterError *error; - FLTPositionMessage *output = [api position:input error:&error]; - callback(wrapResult([output toMap], error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.seekTo" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTPositionMessage *input = [FLTPositionMessage fromMap:message]; - FlutterError *error; - [api seekTo:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.pause" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTTextureMessage *input = [FLTTextureMessage fromMap:message]; - FlutterError *error; - [api pause:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers" - binaryMessenger:binaryMessenger]; - if (api) { - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FLTMixWithOthersMessage *input = [FLTMixWithOthersMessage fromMap:message]; - FlutterError *error; - [api setMixWithOthers:input error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } -} diff --git a/packages/video_player/video_player/ios/video_player.podspec b/packages/video_player/video_player/ios/video_player.podspec deleted file mode 100644 index 86230f65db7c..000000000000 --- a/packages/video_player/video_player/ios/video_player.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'video_player' - s.version = '0.0.1' - s.summary = 'Flutter Video Player' - s.description = <<-DESC -A Flutter plugin for playing back video on a Widget surface. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/video_player/video_player' } - s.documentation_url = 'https://pub.dev/packages/video_player' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/video_player/video_player/lib/src/closed_caption_file.dart b/packages/video_player/video_player/lib/src/closed_caption_file.dart index 3c7d69b89598..324ffc471ffe 100644 --- a/packages/video_player/video_player/lib/src/closed_caption_file.dart +++ b/packages/video_player/video_player/lib/src/closed_caption_file.dart @@ -2,8 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart' show objectRuntimeType; + import 'sub_rip.dart'; +import 'web_vtt.dart'; + export 'sub_rip.dart' show SubRipCaptionFile; +export 'web_vtt.dart' show WebVTTCaptionFile; /// A structured representation of a parsed closed caption file. /// @@ -15,6 +20,7 @@ export 'sub_rip.dart' show SubRipCaptionFile; /// /// See: /// * [SubRipCaptionFile]. +/// * [WebVTTCaptionFile]. abstract class ClosedCaptionFile { /// The full list of captions from a given file. /// @@ -62,7 +68,7 @@ class Caption { @override String toString() { - return '$runtimeType(' + return '${objectRuntimeType(this, 'Caption')}(' 'number: $number, ' 'start: $start, ' 'end: $end, ' diff --git a/packages/video_player/video_player/lib/src/sub_rip.dart b/packages/video_player/video_player/lib/src/sub_rip.dart index 73cd8266c2e9..7b807cd4d5d9 100644 --- a/packages/video_player/video_player/lib/src/sub_rip.dart +++ b/packages/video_player/video_player/lib/src/sub_rip.dart @@ -16,6 +16,8 @@ class SubRipCaptionFile extends ClosedCaptionFile { : _captions = _parseCaptionsFromSubRipString(fileContents); /// The entire body of the SubRip file. + // TODO(cyanglaz): Remove this public member as it doesn't seem need to exist. + // https://github.com/flutter/flutter/issues/90471 final String fileContents; @override @@ -26,19 +28,21 @@ class SubRipCaptionFile extends ClosedCaptionFile { List _parseCaptionsFromSubRipString(String file) { final List captions = []; - for (List captionLines in _readSubRipFile(file)) { - if (captionLines.length < 3) break; + for (final List captionLines in _readSubRipFile(file)) { + if (captionLines.length < 3) { + break; + } final int captionNumber = int.parse(captionLines[0]); - final _StartAndEnd startAndEnd = - _StartAndEnd.fromSubRipString(captionLines[1]); + final _CaptionRange captionRange = + _CaptionRange.fromSubRipString(captionLines[1]); final String text = captionLines.sublist(2).join('\n'); final Caption newCaption = Caption( number: captionNumber, - start: startAndEnd.start, - end: startAndEnd.end, + start: captionRange.start, + end: captionRange.end, text: text, ); if (newCaption.start != newCaption.end) { @@ -49,21 +53,21 @@ List _parseCaptionsFromSubRipString(String file) { return captions; } -class _StartAndEnd { +class _CaptionRange { + _CaptionRange(this.start, this.end); + final Duration start; final Duration end; - _StartAndEnd(this.start, this.end); - // Assumes format from an SubRip file. // For example: // 00:01:54,724 --> 00:01:56,760 - static _StartAndEnd fromSubRipString(String line) { + static _CaptionRange fromSubRipString(String line) { final RegExp format = RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp); if (!format.hasMatch(line)) { - return _StartAndEnd(Duration.zero, Duration.zero); + return _CaptionRange(Duration.zero, Duration.zero); } final List times = line.split(_subRipArrow); @@ -71,7 +75,7 @@ class _StartAndEnd { final Duration start = _parseSubRipTimestamp(times[0]); final Duration end = _parseSubRipTimestamp(times[1]); - return _StartAndEnd(start, end); + return _CaptionRange(start, end); } } diff --git a/packages/video_player/video_player/lib/src/web_vtt.dart b/packages/video_player/video_player/lib/src/web_vtt.dart new file mode 100644 index 000000000000..5527e62b69f1 --- /dev/null +++ b/packages/video_player/video_player/lib/src/web_vtt.dart @@ -0,0 +1,215 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:html/dom.dart'; +import 'package:html/parser.dart' as html_parser; + +import 'closed_caption_file.dart'; + +/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format. +/// See: https://en.wikipedia.org/wiki/WebVTT +class WebVTTCaptionFile extends ClosedCaptionFile { + /// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in + /// the WebVTT file format. + /// * See: https://en.wikipedia.org/wiki/WebVTT + WebVTTCaptionFile(String fileContents) + : _captions = _parseCaptionsFromWebVTTString(fileContents); + + @override + List get captions => _captions; + + final List _captions; +} + +List _parseCaptionsFromWebVTTString(String file) { + final List captions = []; + + // Ignore metadata + final Set metadata = {'HEADER', 'NOTE', 'REGION', 'WEBVTT'}; + + int captionNumber = 1; + for (final List captionLines in _readWebVTTFile(file)) { + // CaptionLines represent a complete caption. + // E.g + // [ + // [00:00.000 --> 01:24.000 align:center] + // ['Introduction'] + // ] + // If caption has just header or time, but no text, `captionLines.length` will be 1. + if (captionLines.length < 2) { + continue; + } + + // If caption has header equal metadata, ignore. + final String metadaType = captionLines[0].split(' ')[0]; + if (metadata.contains(metadaType)) { + continue; + } + + // Caption has header + final bool hasHeader = captionLines.length > 2; + if (hasHeader) { + final int? tryParseCaptionNumber = int.tryParse(captionLines[0]); + if (tryParseCaptionNumber != null) { + captionNumber = tryParseCaptionNumber; + } + } + + final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString( + hasHeader ? captionLines[1] : captionLines[0], + ); + + if (captionRange == null) { + continue; + } + + final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n'); + + // TODO(cyanglaz): Handle special syntax in VTT captions. + // https://github.com/flutter/flutter/issues/90007. + final String textWithoutFormat = _extractTextFromHtml(text); + + final Caption newCaption = Caption( + number: captionNumber, + start: captionRange.start, + end: captionRange.end, + text: textWithoutFormat, + ); + captions.add(newCaption); + captionNumber++; + } + + return captions; +} + +class _CaptionRange { + _CaptionRange(this.start, this.end); + + final Duration start; + final Duration end; + + // Assumes format from an VTT file. + // For example: + // 00:09.000 --> 00:11.000 + static _CaptionRange? fromWebVTTString(String line) { + final RegExp format = + RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp); + + if (!format.hasMatch(line)) { + return null; + } + + final List times = line.split(_webVTTArrow); + + final Duration? start = _parseWebVTTTimestamp(times[0]); + final Duration? end = _parseWebVTTTimestamp(times[1]); + + if (start == null || end == null) { + return null; + } + + return _CaptionRange(start, end); + } +} + +String _extractTextFromHtml(String htmlString) { + final Document document = html_parser.parse(htmlString); + final Element? body = document.body; + if (body == null) { + return ''; + } + final Element? bodyElement = html_parser.parse(body.text).documentElement; + return bodyElement?.text ?? ''; +} + +// Parses a time stamp in an VTT file into a Duration. +// +// Returns `null` if `timestampString` is in an invalid format. +// +// For example: +// +// _parseWebVTTTimestamp('00:01:08.430') +// returns +// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430) +Duration? _parseWebVTTTimestamp(String timestampString) { + if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) { + return null; + } + + final List dotSections = timestampString.split('.'); + final List timeComponents = dotSections[0].split(':'); + + // Validating and parsing the `timestampString`, invalid format will result this method + // to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid + // WebVTT timestamp format. + if (timeComponents.length > 3 || timeComponents.length < 2) { + return null; + } + int hours = 0; + if (timeComponents.length == 3) { + final String hourString = timeComponents.removeAt(0); + if (hourString.length < 2) { + return null; + } + hours = int.parse(hourString); + } + final int minutes = int.parse(timeComponents.removeAt(0)); + if (minutes < 0 || minutes > 59) { + return null; + } + final int seconds = int.parse(timeComponents.removeAt(0)); + if (seconds < 0 || seconds > 59) { + return null; + } + + final List milisecondsStyles = dotSections[1].split(' '); + + // TODO(cyanglaz): Handle caption styles. + // https://github.com/flutter/flutter/issues/90009. + // ```dart + // if (milisecondsStyles.length > 1) { + // List styles = milisecondsStyles.sublist(1); + // } + // ``` + // For a better readable code style, style parsing should happen before + // calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134. + final int milliseconds = int.parse(milisecondsStyles[0]); + + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + ); +} + +// Reads on VTT file and splits it into Lists of strings where each list is one +// caption. +List> _readWebVTTFile(String file) { + final List lines = LineSplitter.split(file).toList(); + + final List> captionStrings = >[]; + List currentCaption = []; + int lineIndex = 0; + for (final String line in lines) { + final bool isLineBlank = line.trim().isEmpty; + if (!isLineBlank) { + currentCaption.add(line); + } + + if (isLineBlank || lineIndex == lines.length - 1) { + captionStrings.add(currentCaption); + currentCaption = []; + } + + lineIndex += 1; + } + + return captionStrings; +} + +const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})'; +const String _webVTTArrow = r' --> '; diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index fe3437593a81..96aa881aba39 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -4,23 +4,32 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'src/closed_caption_file.dart'; + export 'package:video_player_platform_interface/video_player_platform_interface.dart' show DurationRange, DataSourceType, VideoFormat, VideoPlayerOptions; -import 'src/closed_caption_file.dart'; export 'src/closed_caption_file.dart'; -final VideoPlayerPlatform _videoPlayerPlatform = VideoPlayerPlatform.instance - // This will clear all open videos on the platform when a full restart is - // performed. - ..init(); +VideoPlayerPlatform? _lastVideoPlayerPlatform; + +VideoPlayerPlatform get _videoPlayerPlatform { + final VideoPlayerPlatform currentInstance = VideoPlayerPlatform.instance; + if (_lastVideoPlayerPlatform != currentInstance) { + // This will clear all open videos on the platform when a full restart is + // performed. + currentInstance.init(); + _lastVideoPlayerPlatform = currentInstance; + } + return currentInstance; +} /// The duration, current position, buffering state, error state and settings /// of a [VideoPlayerController]. @@ -32,6 +41,7 @@ class VideoPlayerValue { this.size = Size.zero, this.position = Duration.zero, this.caption = Caption.none, + this.captionOffset = Duration.zero, this.buffered = const [], this.isInitialized = false, this.isPlaying = false, @@ -39,6 +49,7 @@ class VideoPlayerValue { this.isBuffering = false, this.volume = 1.0, this.playbackSpeed = 1.0, + this.rotationCorrection = 0, this.errorDescription, }); @@ -53,6 +64,10 @@ class VideoPlayerValue { isInitialized: false, errorDescription: errorDescription); + /// This constant is just to indicate that parameter is not passed to [copyWith] + /// workaround for this issue https://github.com/dart-lang/language/issues/2009 + static const String _defaultErrorDescription = 'defaultErrorDescription'; + /// The total duration of the video. /// /// The duration is [Duration.zero] if the video hasn't been initialized. @@ -67,6 +82,11 @@ class VideoPlayerValue { /// [position], this will be a [Caption.none] object. final Caption caption; + /// The [Duration] that should be used to offset the current [position] to get the correct [Caption]. + /// + /// Defaults to Duration.zero. + final Duration captionOffset; + /// The currently buffered ranges. final List buffered; @@ -93,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; @@ -118,12 +141,13 @@ 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, Duration? position, Caption? caption, + Duration? captionOffset, List? buffered, bool? isInitialized, bool? isPlaying, @@ -131,13 +155,15 @@ class VideoPlayerValue { bool? isBuffering, double? volume, double? playbackSpeed, - String? errorDescription, + int? rotationCorrection, + String? errorDescription = _defaultErrorDescription, }) { return VideoPlayerValue( duration: duration ?? this.duration, size: size ?? this.size, position: position ?? this.position, caption: caption ?? this.caption, + captionOffset: captionOffset ?? this.captionOffset, buffered: buffered ?? this.buffered, isInitialized: isInitialized ?? this.isInitialized, isPlaying: isPlaying ?? this.isPlaying, @@ -145,17 +171,21 @@ class VideoPlayerValue { isBuffering: isBuffering ?? this.isBuffering, volume: volume ?? this.volume, playbackSpeed: playbackSpeed ?? this.playbackSpeed, - errorDescription: errorDescription ?? this.errorDescription, + rotationCorrection: rotationCorrection ?? this.rotationCorrection, + errorDescription: errorDescription != _defaultErrorDescription + ? errorDescription + : this.errorDescription, ); } @override String toString() { - return '$runtimeType(' + return '${objectRuntimeType(this, 'VideoPlayerValue')}(' 'duration: $duration, ' 'size: $size, ' 'position: $position, ' 'caption: $caption, ' + 'captionOffset: $captionOffset, ' 'buffered: [${buffered.join(', ')}], ' 'isInitialized: $isInitialized, ' 'isPlaying: $isPlaying, ' @@ -184,10 +214,13 @@ 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 {}, + httpHeaders = const {}, super(VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from obtained from @@ -202,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, + this.httpHeaders = const {}, + }) : _closedCaptionFileFuture = closedCaptionFile, + dataSourceType = DataSourceType.network, package = null, super(VideoPlayerValue(duration: Duration.zero)); @@ -214,12 +248,13 @@ 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, - httpHeaders = const {}, + httpHeaders = const {}, super(VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from a contentUri. @@ -227,14 +262,15 @@ 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, formatHint = null, - httpHeaders = const {}, + httpHeaders = const {}, super(VideoPlayerValue(duration: Duration.zero)); /// The URI to the video file. This will be in different formats depending on @@ -260,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 @@ -286,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; @@ -341,7 +375,9 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith( duration: event.duration, size: event.size, + rotationCorrection: event.rotationCorrection, isInitialized: event.duration != null, + errorDescription: null, ); initializingCompleter.complete(null); _applyLooping(); @@ -369,11 +405,8 @@ class VideoPlayerController extends ValueNotifier { } } - if (closedCaptionFile != null) { - if (_closedCaptionFile == null) { - _closedCaptionFile = await closedCaptionFile; - } - value = value.copyWith(caption: _getCaptionAt(value.position)); + if (_closedCaptionFileFuture != null) { + await _updateClosedCaptionWithFuture(_closedCaptionFileFuture); } void errorListener(Object obj) { @@ -393,6 +426,10 @@ class VideoPlayerController extends ValueNotifier { @override Future dispose() async { + if (_isDisposed) { + return; + } + if (_creatingCompleter != null) { await _creatingCompleter!.future; if (!_isDisposed) { @@ -401,7 +438,7 @@ class VideoPlayerController extends ValueNotifier { await _eventSubscription?.cancel(); await _videoPlayerPlatform.dispose(_textureId); } - _lifeCycleObserver.dispose(); + _lifeCycleObserver?.dispose(); } _isDisposed = true; super.dispose(); @@ -490,7 +527,9 @@ class VideoPlayerController extends ValueNotifier { // Setting the playback speed on iOS will trigger the video to play. We // prevent this from happening by not applying the playback speed until // the video is manually played from Flutter. - if (!value.isPlaying) return; + if (!value.isPlaying) { + return; + } await _videoPlayerPlatform.setPlaybackSpeed( _textureId, @@ -567,6 +606,22 @@ class VideoPlayerController extends ValueNotifier { await _applyPlaybackSpeed(); } + /// Sets the caption offset. + /// + /// The [offset] will be used when getting the correct caption for a specific position. + /// The [offset] can be positive or negative. + /// + /// The values will be handled as follows: + /// * 0: This is the default behaviour. No offset will be applied. + /// * >0: The caption will have a negative offset. So you will get caption text from the past. + /// * <0: The caption will have a positive offset. So you will get caption text from the future. + void setCaptionOffset(Duration offset) { + value = value.copyWith( + captionOffset: offset, + caption: _getCaptionAt(value.position), + ); + } + /// The closed caption based on the current [position] in the video. /// /// If there are no closed captions at the current [position], this will @@ -579,9 +634,10 @@ class VideoPlayerController extends ValueNotifier { return Caption.none; } - // TODO: This would be more efficient as a binary search. - for (final caption in _closedCaptionFile!.captions) { - if (caption.start <= position && caption.end >= position) { + final Duration delayedPosition = position + value.captionOffset; + // TODO(johnsonmh): This would be more efficient as a binary search. + for (final Caption caption in _closedCaptionFile!.captions) { + if (caption.start <= delayedPosition && caption.end >= delayedPosition) { return caption; } } @@ -589,9 +645,33 @@ 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); - value = value.copyWith(caption: _getCaptionAt(position)); + value = value.copyWith( + position: position, + caption: _getCaptionAt(position), + ); } @override @@ -640,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. - 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 { @@ -693,10 +773,29 @@ 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. /// @@ -738,7 +837,7 @@ class VideoProgressColors { } class _VideoScrubber extends StatefulWidget { - _VideoScrubber({ + const _VideoScrubber({ required this.child, required this.controller, }); @@ -758,7 +857,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { @override Widget build(BuildContext context) { void seekToRelativePosition(Offset globalPosition) { - final RenderBox box = context.findRenderObject() as RenderBox; + final RenderBox box = context.findRenderObject()! as RenderBox; final Offset tapPos = box.globalToLocal(globalPosition); final double relative = tapPos.dx / box.size.width; final Duration position = controller.value.duration * relative; @@ -784,7 +883,8 @@ class _VideoScrubberState extends State<_VideoScrubber> { seekToRelativePosition(details.globalPosition); }, onHorizontalDragEnd: (DragEndDetails details) { - if (_controllerWasPlaying) { + if (_controllerWasPlaying && + controller.value.position != controller.value.duration) { controller.play(); } }, @@ -812,12 +912,13 @@ class VideoProgressIndicator extends StatefulWidget { /// Defaults will be used for everything except [controller] if they're not /// provided. [allowScrubbing] defaults to false, and [padding] will default /// to `top: 5.0`. - VideoProgressIndicator( + 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. @@ -841,7 +942,7 @@ class VideoProgressIndicator extends StatefulWidget { final EdgeInsets padding; @override - _VideoProgressIndicatorState createState() => _VideoProgressIndicatorState(); + State createState() => _VideoProgressIndicatorState(); } class _VideoProgressIndicatorState extends State { @@ -880,7 +981,7 @@ class _VideoProgressIndicatorState extends State { final int position = controller.value.position.inMilliseconds; int maxBuffering = 0; - for (DurationRange range in controller.value.buffered) { + for (final DurationRange range in controller.value.buffered) { final int end = range.end.inMilliseconds; if (end > maxBuffering) { maxBuffering = end; @@ -915,8 +1016,8 @@ class _VideoProgressIndicatorState extends State { ); if (widget.allowScrubbing) { return _VideoScrubber( - child: paddedProgressIndicator, controller: controller, + child: paddedProgressIndicator, ); } else { return paddedProgressIndicator; @@ -962,9 +1063,9 @@ class ClosedCaption extends StatelessWidget { @override Widget build(BuildContext context) { - final text = this.text; + final String? text = this.text; if (text == null || text.isEmpty) { - return SizedBox.shrink(); + return const SizedBox.shrink(); } final TextStyle effectiveTextStyle = textStyle ?? @@ -976,14 +1077,14 @@ class ClosedCaption extends StatelessWidget { return Align( alignment: Alignment.bottomCenter, child: Padding( - padding: EdgeInsets.only(bottom: 24.0), + padding: const EdgeInsets.only(bottom: 24.0), child: DecoratedBox( decoration: BoxDecoration( - color: Color(0xB8000000), + color: const Color(0xB8000000), borderRadius: BorderRadius.circular(2.0), ), child: Padding( - padding: EdgeInsets.symmetric(horizontal: 2.0), + padding: const EdgeInsets.symmetric(horizontal: 2.0), child: Text(text, style: effectiveTextStyle), ), ), diff --git a/packages/video_player/video_player/pigeons/messages.dart b/packages/video_player/video_player/pigeons/messages.dart deleted file mode 100644 index e893aaa6830d..000000000000 --- a/packages/video_player/video_player/pigeons/messages.dart +++ /dev/null @@ -1,69 +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 = '../video_player_platform_interface/lib/messages.dart'; - opts.dartTestOut = '../video_player_platform_interface/lib/test.dart'; - opts.objcHeaderOut = 'ios/Classes/messages.h'; - opts.objcSourceOut = 'ios/Classes/messages.m'; - opts.objcOptions.prefix = 'FLT'; - opts.javaOut = - 'android/src/main/java/io/flutter/plugins/videoplayer/Messages.java'; - opts.javaOptions.package = 'io.flutter.plugins.videoplayer'; -} diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 926add50f43c..bb0e8a8ec581 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -1,40 +1,33 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. -repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player +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.4 +version: 2.4.5 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.videoplayer - pluginClass: VideoPlayerPlugin + default_package: video_player_android ios: - pluginClass: FLTVideoPlayerPlugin + default_package: video_player_avfoundation web: default_package: video_player_web dependencies: flutter: sdk: flutter - meta: ^1.3.0 - video_player_platform_interface: ^4.2.0 - # The design on https://flutter.dev/go/federated-plugins was to leave - # this constraint as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. The exact value doesn't matter since - # the constraints on the interface pins it. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 + html: ^0.15.0 + video_player_android: ^2.3.5 + video_player_avfoundation: ^2.2.17 + video_player_platform_interface: ^5.1.1 video_player_web: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 - pigeon: ^0.1.21 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 369a3b362557..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 @@ -8,7 +8,7 @@ import 'package:video_player/src/closed_caption_file.dart'; void main() { group('ClosedCaptionFile', () { test('toString()', () { - final Caption caption = const Caption( + const Caption caption = Caption( number: 1, start: Duration(seconds: 1), end: Duration(seconds: 2), @@ -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 5808e0b9d2e3..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 @@ -14,19 +14,19 @@ void main() { final Caption firstCaption = parsedFile.captions.first; expect(firstCaption.number, 1); - expect(firstCaption.start, Duration(seconds: 6)); - expect(firstCaption.end, Duration(seconds: 12, milliseconds: 74)); + expect(firstCaption.start, const Duration(seconds: 6)); + expect(firstCaption.end, const Duration(seconds: 12, milliseconds: 74)); expect(firstCaption.text, 'This is a test file'); final Caption secondCaption = parsedFile.captions[1]; expect(secondCaption.number, 2); expect( secondCaption.start, - Duration(minutes: 1, seconds: 54, milliseconds: 724), + const Duration(minutes: 1, seconds: 54, milliseconds: 724), ); expect( secondCaption.end, - Duration(minutes: 1, seconds: 56, milliseconds: 760), + const Duration(minutes: 1, seconds: 56, milliseconds: 760), ); expect(secondCaption.text, '- Hello.\n- Yes?'); @@ -34,11 +34,11 @@ void main() { expect(thirdCaption.number, 3); expect( thirdCaption.start, - Duration(minutes: 1, seconds: 56, milliseconds: 884), + const Duration(minutes: 1, seconds: 56, milliseconds: 884), ); expect( thirdCaption.end, - Duration(minutes: 1, seconds: 58, milliseconds: 954), + const Duration(minutes: 1, seconds: 58, milliseconds: 954), ); expect( thirdCaption.text, @@ -49,15 +49,15 @@ void main() { expect(fourthCaption.number, 4); expect( fourthCaption.start, - Duration(hours: 1, minutes: 1, seconds: 59, milliseconds: 84), + const Duration(hours: 1, minutes: 1, seconds: 59, milliseconds: 84), ); expect( fourthCaption.end, - Duration(hours: 1, minutes: 2, seconds: 1, milliseconds: 552), + const Duration(hours: 1, minutes: 2, seconds: 1, milliseconds: 552), ); expect( fourthCaption.text, - '- [ Machinery Beeping ]\n- I\'m not sure what that was,', + "- [ Machinery Beeping ]\n- I'm not sure what that was,", ); }); @@ -68,8 +68,8 @@ void main() { final Caption firstCaption = parsedFile.captions.single; expect(firstCaption.number, 2); - expect(firstCaption.start, Duration(seconds: 15)); - expect(firstCaption.end, Duration(seconds: 17, milliseconds: 74)); + expect(firstCaption.start, const Duration(seconds: 15)); + expect(firstCaption.end, const Duration(seconds: 17, milliseconds: 74)); expect(firstCaption.text, 'This one is valid'); }); } diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 13bfd7be7889..af0886fdec18 100644 --- a/packages/video_player/video_player/test/video_player_initialization_test.dart +++ b/packages/video_player/video_player/test/video_player_initialization_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'video_player_test.dart' show FakeVideoPlayerPlatform; @@ -12,7 +13,9 @@ void main() { // in this file. test('plugin initialized', () async { TestWidgetsFlutterBinding.ensureInitialized(); - FakeVideoPlayerPlatform fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + final FakeVideoPlayerPlatform fakeVideoPlayerPlatform = + FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', 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 a929c6827fd0..7837a0bec328 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -4,21 +4,21 @@ 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/messages.dart'; -import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class FakeController extends ValueNotifier implements VideoPlayerController { FakeController() : super(VideoPlayerValue(duration: Duration.zero)); + FakeController.value(VideoPlayerValue value) : super(value); + @override Future dispose() async { super.dispose(); @@ -31,7 +31,7 @@ class FakeController extends ValueNotifier String get dataSource => ''; @override - Map get httpHeaders => {}; + Map get httpHeaders => {}; @override DataSourceType get dataSourceType => DataSourceType.file; @@ -71,6 +71,14 @@ class FakeController extends ValueNotifier @override VideoPlayerOptions? get videoPlayerOptions => null; + + @override + void setCaptionOffset(Duration delay) {} + + @override + Future setClosedCaptionFile( + Future? closedCaptionFile, + ) async {} } Future _loadClosedCaption() async => @@ -80,13 +88,13 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { @override List get captions { return [ - Caption( + const Caption( text: 'one', number: 0, start: Duration(milliseconds: 100), end: Duration(milliseconds: 200), ), - Caption( + const Caption( text: 'two', number: 1, start: Duration(milliseconds: 300), @@ -97,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)); @@ -132,10 +153,40 @@ 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, rotationCorrection: 0)); + 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 { - final String text = 'foo'; - await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: text))); + const String text = 'foo'; + await tester + .pumpWidget(const MaterialApp(home: ClosedCaption(text: text))); final Text textWidget = tester.widget(find.text(text)); expect(textWidget.style!.fontSize, 36.0); @@ -143,9 +194,9 @@ void main() { }); testWidgets('uses given text and style', (WidgetTester tester) async { - final String text = 'foo'; - final TextStyle textStyle = TextStyle(fontSize: 14.725); - await tester.pumpWidget(MaterialApp( + const String text = 'foo'; + const TextStyle textStyle = TextStyle(fontSize: 14.725); + await tester.pumpWidget(const MaterialApp( home: ClosedCaption( text: text, textStyle: textStyle, @@ -158,19 +209,20 @@ void main() { }); testWidgets('handles null text', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: null))); + await tester + .pumpWidget(const MaterialApp(home: ClosedCaption(text: null))); expect(find.byType(Text), findsNothing); }); testWidgets('handles empty text', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: ''))); + await tester.pumpWidget(const MaterialApp(home: ClosedCaption(text: ''))); expect(find.byType(Text), findsNothing); }); testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { - final String text = 'foo'; - await tester.pumpWidget(MaterialApp( + const String text = 'foo'; + await tester.pumpWidget(const MaterialApp( home: Scaffold( backgroundColor: Colors.white, body: ClosedCaption(text: text), @@ -187,19 +239,28 @@ void main() { setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; }); 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', ); await controller.initialize(); - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].asset, 'a.avi'); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].packageName, - null); + expect(fakeVideoPlayerPlatform.dataSources[0].asset, 'a.avi'); + expect(fakeVideoPlayerPlatform.dataSources[0].package, null); }); test('network', () async { @@ -209,16 +270,16 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + fakeVideoPlayerPlatform.dataSources[0].formatHint, null, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, - {}, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, ); }); @@ -230,37 +291,37 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, - 'dash', + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, - {}, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, ); }); test('network with some headers', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', - httpHeaders: {'Authorization': 'Bearer token'}, + httpHeaders: {'Authorization': 'Bearer token'}, ); await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + fakeVideoPlayerPlatform.dataSources[0].formatHint, null, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, - {'Authorization': 'Bearer token'}, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {'Authorization': 'Bearer token'}, ); }); @@ -268,15 +329,12 @@ void main() { final VideoPlayerController controller = VideoPlayerController.network( 'http://testing.com/invalid_url', ); - try { - late dynamic error; - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) => error = e); - final PlatformException platformEx = error; - expect(platformEx.code, equals('VideoError')); - } finally { - fakeVideoPlayerPlatform.forceInitError = false; - } + + late Object error; + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((Object e) => error = e); + final PlatformException platformEx = error as PlatformException; + expect(platformEx.code, equals('VideoError')); }); test('file', () async { @@ -284,8 +342,20 @@ void main() { VideoPlayerController.file(File('a.avi')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'file://a.avi'); + expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'file://a.avi'); + }); + + test('successful initialize on controller with error clears error', + () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) {}); + expect(controller.value.hasError, equals(true)); + fakeVideoPlayerPlatform.forceInitError = false; + await controller.initialize(); + expect(controller.value.hasError, equals(false)); }); }); @@ -294,8 +364,7 @@ void main() { VideoPlayerController.contentUri(Uri.parse('content://video')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'content://video'); + expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); }); test('dispose', () async { @@ -313,6 +382,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', @@ -478,6 +558,60 @@ void main() { }); }); + group('scrubbing', () { + testWidgets('restarts on release if already playing', + (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + final VideoProgressIndicator progressWidget = + VideoProgressIndicator(controller, allowScrubbing: true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + )); + + await controller.play(); + expect(controller.value.isPlaying, isTrue); + + final Rect progressRect = tester.getRect(find.byWidget(progressWidget)); + await tester.dragFrom(progressRect.center, const Offset(1.0, 0.0)); + await tester.pumpAndSettle(); + + expect(controller.value.position, lessThan(controller.value.duration)); + expect(controller.value.isPlaying, isTrue); + + await controller.pause(); + }); + + testWidgets('does not restart when dragging to end', + (WidgetTester tester) async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + final VideoProgressIndicator progressWidget = + VideoProgressIndicator(controller, allowScrubbing: true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: progressWidget, + )); + + await controller.play(); + expect(controller.value.isPlaying, isTrue); + + final Rect progressRect = tester.getRect(find.byWidget(progressWidget)); + await tester.dragFrom(progressRect.center, progressRect.centerRight); + await tester.pumpAndSettle(); + + expect(controller.value.position, controller.value.duration); + expect(controller.value.isPlaying, isFalse); + }); + }); + group('caption', () { test('works when seeking', () async { final VideoPlayerController controller = VideoPlayerController.network( @@ -498,11 +632,123 @@ void main() { await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, 'two'); + await controller.seekTo(const Duration(milliseconds: 500)); expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, 'two'); + }); + + test('works when seeking with captionOffset positive', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + controller.setCaptionOffset(const Duration(milliseconds: 100)); + expect(controller.value.position, const Duration()); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, 'one'); + + await controller.seekTo(const Duration(milliseconds: 101)); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 250)); + expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, ''); + }); + + test('works when seeking with captionOffset negative', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + closedCaptionFile: _loadClosedCaption(), + ); + + await controller.initialize(); + controller.setCaptionOffset(const Duration(milliseconds: -100)); + expect(controller.value.position, const Duration()); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 100)); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 200)); + expect(controller.value.caption.text, 'one'); + + await controller.seekTo(const Duration(milliseconds: 250)); + expect(controller.value.caption.text, 'one'); + + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'one'); + + await controller.seekTo(const Duration(milliseconds: 301)); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 400)); + expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 500)); + expect(controller.value.caption.text, 'two'); + + await controller.seekTo(const Duration(milliseconds: 600)); + expect(controller.value.caption.text, ''); + + await controller.seekTo(const Duration(milliseconds: 300)); + expect(controller.value.caption.text, 'one'); + }); + + test('setClosedCapitonFile 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('setClosedCapitonFile 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); }); }); @@ -517,11 +763,11 @@ void main() { expect(controller.value.isPlaying, isFalse); await controller.play(); expect(controller.value.isPlaying, isTrue); - final FakeVideoEventStream fakeVideoEventStream = + final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'completed'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.completed)); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -535,30 +781,29 @@ void main() { await controller.initialize(); expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); - final FakeVideoEventStream fakeVideoEventStream = + final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'bufferingStart'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingStart)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); const Duration bufferStart = Duration(seconds: 0); const Duration bufferEnd = Duration(milliseconds: 500); - fakeVideoEventStream.eventsChannel.sendEvent({ - 'event': 'bufferingUpdate', - 'values': >[ - [bufferStart.inMilliseconds, bufferEnd.inMilliseconds] - ], - }); + fakeVideoEventStream.add(VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(bufferStart, bufferEnd), + ])); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); expect(controller.value.buffered.length, 1); expect(controller.value.buffered[0].toString(), DurationRange(bufferStart, bufferEnd).toString()); - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'bufferingEnd'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); }); @@ -596,6 +841,7 @@ void main() { expect(uninitialized.duration, equals(Duration.zero)); expect(uninitialized.position, equals(Duration.zero)); expect(uninitialized.caption, equals(Caption.none)); + expect(uninitialized.captionOffset, equals(Duration.zero)); expect(uninitialized.buffered, isEmpty); expect(uninitialized.isPlaying, isFalse); expect(uninitialized.isLooping, isFalse); @@ -616,6 +862,7 @@ void main() { expect(error.duration, equals(Duration.zero)); expect(error.position, equals(Duration.zero)); expect(error.caption, equals(Caption.none)); + expect(error.captionOffset, equals(Duration.zero)); expect(error.buffered, isEmpty); expect(error.isPlaying, isFalse); expect(error.isLooping, isFalse); @@ -635,6 +882,7 @@ void main() { const Duration position = Duration(seconds: 1); const Caption caption = Caption( 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)) ]; @@ -650,6 +898,7 @@ void main() { size: size, position: position, caption: caption, + captionOffset: captionOffset, buffered: buffered, isInitialized: isInitialized, isPlaying: isPlaying, @@ -665,6 +914,7 @@ void main() { 'size: Size(400.0, 300.0), ' 'position: 0:00:01.000000, ' 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' + 'captionOffset: 0:00:00.250000, ' 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' 'isInitialized: true, ' 'isPlaying: true, ' @@ -675,66 +925,135 @@ void main() { 'errorDescription: null)'); }); - test('copyWith()', () { - final VideoPlayerValue original = VideoPlayerValue.uninitialized(); - final VideoPlayerValue exactCopy = original.copyWith(); + group('copyWith()', () { + test('exact copy', () { + final VideoPlayerValue original = VideoPlayerValue.uninitialized(); + final VideoPlayerValue exactCopy = original.copyWith(); - expect(exactCopy.toString(), original.toString()); + expect(exactCopy.toString(), original.toString()); + }); + test('errorDescription is not persisted when copy with null', () { + final VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + final VideoPlayerValue copy = original.copyWith(errorDescription: null); + + expect(copy.errorDescription, null); + }); + test('errorDescription is changed when copy with another error', () { + final VideoPlayerValue original = VideoPlayerValue.erroneous('error'); + final VideoPlayerValue copy = + original.copyWith(errorDescription: 'new error'); + + expect(copy.errorDescription, 'new error'); + }); + test('errorDescription is changed when copy with error', () { + final VideoPlayerValue original = VideoPlayerValue.uninitialized(); + final VideoPlayerValue copy = + original.copyWith(errorDescription: 'new error'); + + expect(copy.errorDescription, 'new error'); + }); }); group('aspectRatio', () { test('640x480 -> 4:3', () { - final value = VideoPlayerValue( + final VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: Size(640, 480), - duration: Duration(seconds: 1), + size: const Size(640, 480), + duration: const Duration(seconds: 1), ); expect(value.aspectRatio, 4 / 3); }); test('no size -> 1.0', () { - final value = VideoPlayerValue( + final VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - duration: Duration(seconds: 1), + duration: const Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); test('height = 0 -> 1.0', () { - final value = VideoPlayerValue( + final VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: Size(640, 0), - duration: Duration(seconds: 1), + size: const Size(640, 0), + duration: const Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); test('width = 0 -> 1.0', () { - final value = VideoPlayerValue( + final VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: Size(0, 480), - duration: Duration(seconds: 1), + size: const Size(0, 480), + duration: const Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); test('negative aspect ratio -> 1.0', () { - final value = VideoPlayerValue( + final VideoPlayerValue value = VideoPlayerValue( isInitialized: true, - size: Size(640, -480), - duration: Duration(seconds: 1), + size: const Size(640, -480), + duration: const Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); }); }); }); + 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( + allowBackgroundPlayback: false, + ), + ); + 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); const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25); - final VideoProgressColors colors = VideoProgressColors( + const VideoProgressColors colors = VideoProgressColors( playedColor: playedColor, bufferedColor: bufferedColor, backgroundColor: backgroundColor); @@ -743,165 +1062,97 @@ 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 TestHostVideoPlayerApi { - FakeVideoPlayerPlatform() { - TestHostVideoPlayerApi.setup(this); - } - +class FakeVideoPlayerPlatform extends VideoPlayerPlatform { Completer initialized = Completer(); List calls = []; - List dataSourceDescriptions = []; - final Map streams = {}; + List dataSources = []; + final Map> streams = + >{}; bool forceInitError = false; int nextTextureId = 0; final Map _positions = {}; @override - TextureMessage create(CreateMessage arg) { + Future create(DataSource dataSource) async { calls.add('create'); - streams[nextTextureId] = FakeVideoEventStream( - nextTextureId, 100, 100, const Duration(seconds: 1), forceInitError); - TextureMessage result = TextureMessage(); - result.textureId = nextTextureId++; - dataSourceDescriptions.add(arg); - return result; + final StreamController stream = StreamController(); + streams[nextTextureId] = stream; + if (forceInitError) { + stream.addError(PlatformException( + code: 'VideoError', message: 'Video player had error XYZ')); + } else { + stream.add(VideoEvent( + eventType: VideoEventType.initialized, + size: const Size(100, 100), + duration: const Duration(seconds: 1))); + } + dataSources.add(dataSource); + return nextTextureId++; } @override - void dispose(TextureMessage arg) { + Future dispose(int textureId) async { calls.add('dispose'); } @override - void initialize() { + Future init() async { calls.add('init'); initialized.complete(true); } @override - void pause(TextureMessage arg) { + Stream videoEventsFor(int textureId) { + return streams[textureId]!.stream; + } + + @override + Future pause(int textureId) async { calls.add('pause'); } @override - void play(TextureMessage arg) { + Future play(int textureId) async { calls.add('play'); } @override - PositionMessage position(TextureMessage arg) { + Future getPosition(int textureId) async { calls.add('position'); - final Duration position = - _positions[arg.textureId] ?? const Duration(seconds: 0); - return PositionMessage()..position = position.inMilliseconds; + return _positions[textureId] ?? const Duration(seconds: 0); } @override - void seekTo(PositionMessage arg) { + Future seekTo(int textureId, Duration position) async { calls.add('seekTo'); - _positions[arg.textureId!] = Duration(milliseconds: arg.position!); + _positions[textureId] = position; } @override - void setLooping(LoopingMessage arg) { + Future setLooping(int textureId, bool looping) async { calls.add('setLooping'); } @override - void setVolume(VolumeMessage arg) { + Future setVolume(int textureId, double volume) async { calls.add('setVolume'); } @override - void setPlaybackSpeed(PlaybackSpeedMessage arg) { + Future setPlaybackSpeed(int textureId, double speed) async { calls.add('setPlaybackSpeed'); } @override - void setMixWithOthers(MixWithOthersMessage arg) { + Future setMixWithOthers(bool mixWithOthers) async { calls.add('setMixWithOthers'); } } -class FakeVideoEventStream { - FakeVideoEventStream(this.textureId, this.width, this.height, this.duration, - this.initWithError) { - eventsChannel = FakeEventsChannel( - 'flutter.io/videoPlayer/videoEvents$textureId', onListen); - } - - int textureId; - int width; - int height; - Duration duration; - bool initWithError; - late FakeEventsChannel eventsChannel; - - void onListen() { - if (!initWithError) { - eventsChannel.sendEvent({ - 'event': 'initialized', - 'duration': duration.inMilliseconds, - 'width': width, - 'height': height, - }); - } else { - eventsChannel.sendError('VideoError', 'Video player had error XYZ'); - } - } -} - -class FakeEventsChannel { - FakeEventsChannel(String name, this.onListen) { - eventsMethodChannel = MethodChannel(name); - eventsMethodChannel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel eventsMethodChannel; - VoidCallback onListen; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'listen': - onListen(); - break; - } - return Future.sync(() {}); - } - - void sendEvent(dynamic event) { - _sendMessage(const StandardMethodCodec().encodeSuccessEnvelope(event)); - } - - void sendError(String code, [String? message, dynamic details]) { - _sendMessage(const StandardMethodCodec().encodeErrorEnvelope( - code: code, - message: message, - details: details, - )); - } - - void _sendMessage(ByteData data) { - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - eventsMethodChannel.name, data, (ByteData? data) {}); - } -} - /// 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/test/web_vtt_test.dart b/packages/video_player/video_player/test/web_vtt_test.dart new file mode 100644 index 000000000000..bde629219484 --- /dev/null +++ b/packages/video_player/video_player/test/web_vtt_test.dart @@ -0,0 +1,261 @@ +// Copyright 2013 The Flutter Authors. 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/src/closed_caption_file.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + group('Parse VTT file', () { + WebVTTCaptionFile parsedFile; + + test('with Metadata', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_metadata); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].start, const Duration(seconds: 1)); + expect(parsedFile.captions[0].end, + const Duration(seconds: 2, milliseconds: 500)); + expect(parsedFile.captions[0].text, 'We are in New York City'); + }); + + test('with Multiline', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_multiline); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].start, + const Duration(seconds: 2, milliseconds: 800)); + expect(parsedFile.captions[0].end, + const Duration(seconds: 3, milliseconds: 283)); + expect(parsedFile.captions[0].text, + '— It will perforate your stomach.\n— You could die.'); + }); + + test('with styles tags', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_styles); + expect(parsedFile.captions.length, 3); + + 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].text, + "You know I'm so excited my glasses are falling off here."); + }); + + test('with subtitling features', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_subtitling_features); + expect(parsedFile.captions.length, 3); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, const Duration(seconds: 4)); + expect(parsedFile.captions.last.end, const Duration(seconds: 5)); + expect(parsedFile.captions.last.text, 'Transcrit par Célestes™'); + }); + + test('with [hours]:[minutes]:[seconds].[milliseconds].', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_hours); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, const Duration(seconds: 1)); + expect(parsedFile.captions.last.end, const Duration(seconds: 2)); + expect(parsedFile.captions.last.text, 'This is a test.'); + }); + + test('with [minutes]:[seconds].[milliseconds].', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_without_hours); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, const Duration(seconds: 3)); + expect(parsedFile.captions.last.end, const Duration(seconds: 4)); + expect(parsedFile.captions.last.text, 'This is a test.'); + }); + + test('with invalid seconds format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_seconds); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid minutes format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_minutes); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid hours format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_hours); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid component length returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_time_component_too_long); + expect(parsedFile.captions, isEmpty); + + parsedFile = WebVTTCaptionFile(_time_component_too_short); + expect(parsedFile.captions, isEmpty); + }); + }); + + test('Parses VTT file with malformed input.', () { + final ClosedCaptionFile parsedFile = WebVTTCaptionFile(_malformedVTT); + + expect(parsedFile.captions.length, 1); + + 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.text, 'Valid'); + }); +} + +/// See https://www.w3.org/TR/webvtt1/#introduction-comments +const String _valid_vtt_with_metadata = ''' +WEBVTT Kind: captions; Language: en + +REGION +id:bill +width:40% +lines:3 +regionanchor:100%,100% +viewportanchor:90%,90% +scroll:up + +NOTE +This file was written by Jill. I hope +you enjoy reading it. Some things to +bear in mind: +- I was lip-reading, so the cues may +not be 100% accurate +- I didn’t pay too close attention to +when the cues should start or end. + +1 +00:01.000 --> 00:02.500 +We are in New York City +'''; + +/// See https://www.w3.org/TR/webvtt1/#introduction-multiple-lines +const String _valid_vtt_with_multiline = ''' +WEBVTT + +2 +00:02.800 --> 00:03.283 +— It will perforate your stomach. +— You could die. + +'''; + +/// See https://www.w3.org/TR/webvtt1/#styling +const String _valid_vtt_with_styles = ''' +WEBVTT + +00:05.200 --> 00:06.000 align:start size:50% +You know I'm so excited my glasses are falling off here. + +00:00:06.050 --> 00:00:06.150 +I have a different time! + +00:06.200 --> 00:06.900 +This is yellow text on a blue background + +'''; + +//See https://www.w3.org/TR/webvtt1/#introduction-other-features +const String _valid_vtt_with_subtitling_features = ''' +WEBVTT + +test +00:00.000 --> 00:02.000 +This is a test. + +Slide 1 +00:00:00.000 --> 00:00:10.700 +Title Slide + +crédit de transcription +00:04.000 --> 00:05.000 +Transcrit par Célestes™ + +'''; + +/// With format [hours]:[minutes]:[seconds].[milliseconds] +const String _valid_vtt_with_hours = ''' +WEBVTT + +test +00:00:01.000 --> 00:00:02.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _invalid_seconds = ''' +WEBVTT + +60:00:000.000 --> 60:02:000.000 +This is a test. + +'''; + +/// Invalid minutes format. +const String _invalid_minutes = ''' +WEBVTT + +60:60:00.000 --> 60:70:00.000 +This is a test. + +'''; + +/// Invalid hours format. +const String _invalid_hours = ''' +WEBVTT + +5:00:00.000 --> 5:02:00.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _time_component_too_long = ''' +WEBVTT + +60:00:00:00.000 --> 60:02:00:00.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _time_component_too_short = ''' +WEBVTT + +60:00.000 --> 60:02.000 +This is a test. + +'''; + +/// With format [minutes]:[seconds].[milliseconds] +const String _valid_vtt_without_hours = ''' +WEBVTT + +00:03.000 --> 00:04.000 +This is a test. + +'''; + +const String _malformedVTT = ''' + +WEBVTT Kind: captions; Language: en + +00:09.000--> 00:11.430 +This one should be ignored because the arrow needs a space. + +00:13.000 --> 00:16.000 +Valid + +00:16.000 --> 00:8.000 +This one should be ignored because the time is missing a digit. + +'''; diff --git a/packages/video_player/video_player/video_player_android.iml b/packages/video_player/video_player/video_player_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/video_player/video_player/video_player_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/sensors/AUTHORS b/packages/video_player/video_player_android/AUTHORS similarity index 100% rename from packages/sensors/AUTHORS rename to packages/video_player/video_player_android/AUTHORS diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md new file mode 100644 index 000000000000..0c839961d6f7 --- /dev/null +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -0,0 +1,40 @@ +## 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. + +## 2.2.17 + +* Splits from `video_player` as a federated implementation. diff --git a/packages/video_player/video_player_android/CONTRIBUTING.md b/packages/video_player/video_player_android/CONTRIBUTING.md new file mode 100644 index 000000000000..e06f2233278b --- /dev/null +++ b/packages/video_player/video_player_android/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Updating pigeon-generated files + +If you update files in the pigeons/ directory, run the following +command in this directory: + +```bash +flutter pub upgrade +flutter pub run pigeon --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 `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/LICENSE b/packages/video_player/video_player_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_android/README.md b/packages/video_player/video_player_android/README.md new file mode 100644 index 000000000000..28bd66f89c64 --- /dev/null +++ b/packages/video_player/video_player_android/README.md @@ -0,0 +1,11 @@ +# video\_player\_android + +The Android implementation of [`video_player`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `video_player` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/video_player +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle new file mode 100644 index 000000000000..8c1aaaf5f923 --- /dev/null +++ b/packages/video_player/video_player_android/android/build.gradle @@ -0,0 +1,68 @@ +group 'io.flutter.plugins.videoplayer' +version '1.0-SNAPSHOT' +def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.1' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +project.getTasks().withType(JavaCompile){ + options.compilerArgs.addAll(args) +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + dependencies { + implementation 'com.google.android.exoplayer:exoplayer-core:2.17.1' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.17.1' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.17.1' + implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.17.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'org.mockito:mockito-inline:3.9.0' + testImplementation 'org.robolectric:robolectric:4.5' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} \ No newline at end of file diff --git a/packages/video_player/video_player/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/video_player/video_player/android/gradle/wrapper/gradle-wrapper.properties rename to packages/video_player/video_player_android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/video_player/video_player_android/android/settings.gradle b/packages/video_player/video_player_android/android/settings.gradle new file mode 100644 index 000000000000..00681714f7d8 --- /dev/null +++ b/packages/video_player/video_player_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'video_player_android' diff --git a/packages/video_player/video_player/android/src/main/AndroidManifest.xml b/packages/video_player/video_player_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/video_player/video_player/android/src/main/AndroidManifest.xml rename to packages/video_player/video_player_android/android/src/main/AndroidManifest.xml diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java similarity index 100% rename from packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java rename to packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java 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 new file mode 100644 index 000000000000..6593ebf9c22a --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java @@ -0,0 +1,945 @@ +// Copyright 2013 The Flutter 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 (v2.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.videoplayer; + +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.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class Messages { + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class TextureMessage { + private @NonNull Long textureId; + + public @NonNull Long getTextureId() { + return textureId; + } + + public void setTextureId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"textureId\" is null."); + } + this.textureId = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private TextureMessage() {} + + public static class Builder { + private @Nullable Long textureId; + + public @NonNull Builder setTextureId(@NonNull Long setterArg) { + this.textureId = setterArg; + return this; + } + + public @NonNull TextureMessage build() { + TextureMessage pigeonReturn = new TextureMessage(); + pigeonReturn.setTextureId(textureId); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + return toMapResult; + } + + static @NonNull TextureMessage fromMap(@NonNull Map map) { + TextureMessage pigeonResult = new TextureMessage(); + Object textureId = map.get("textureId"); + pigeonResult.setTextureId( + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class LoopingMessage { + private @NonNull Long textureId; + + public @NonNull Long getTextureId() { + return textureId; + } + + public void setTextureId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"textureId\" is null."); + } + this.textureId = setterArg; + } + + private @NonNull Boolean isLooping; + + public @NonNull Boolean getIsLooping() { + return isLooping; + } + + public void setIsLooping(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"isLooping\" is null."); + } + this.isLooping = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private LoopingMessage() {} + + public static class Builder { + private @Nullable Long textureId; + + public @NonNull Builder setTextureId(@NonNull Long setterArg) { + this.textureId = setterArg; + return this; + } + + private @Nullable Boolean isLooping; + + public @NonNull Builder setIsLooping(@NonNull Boolean setterArg) { + this.isLooping = setterArg; + return this; + } + + public @NonNull LoopingMessage build() { + LoopingMessage pigeonReturn = new LoopingMessage(); + pigeonReturn.setTextureId(textureId); + pigeonReturn.setIsLooping(isLooping); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + toMapResult.put("isLooping", isLooping); + return toMapResult; + } + + static @NonNull LoopingMessage fromMap(@NonNull Map map) { + LoopingMessage pigeonResult = new LoopingMessage(); + Object textureId = map.get("textureId"); + pigeonResult.setTextureId( + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); + Object isLooping = map.get("isLooping"); + pigeonResult.setIsLooping((Boolean) isLooping); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class VolumeMessage { + private @NonNull Long textureId; + + public @NonNull Long getTextureId() { + return textureId; + } + + public void setTextureId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"textureId\" is null."); + } + this.textureId = setterArg; + } + + private @NonNull Double volume; + + public @NonNull Double getVolume() { + return volume; + } + + public void setVolume(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"volume\" is null."); + } + this.volume = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private VolumeMessage() {} + + public static class Builder { + private @Nullable Long textureId; + + public @NonNull Builder setTextureId(@NonNull Long setterArg) { + this.textureId = setterArg; + return this; + } + + private @Nullable Double volume; + + public @NonNull Builder setVolume(@NonNull Double setterArg) { + this.volume = setterArg; + return this; + } + + public @NonNull VolumeMessage build() { + VolumeMessage pigeonReturn = new VolumeMessage(); + pigeonReturn.setTextureId(textureId); + pigeonReturn.setVolume(volume); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + toMapResult.put("volume", volume); + return toMapResult; + } + + static @NonNull VolumeMessage fromMap(@NonNull Map map) { + VolumeMessage pigeonResult = new VolumeMessage(); + Object textureId = map.get("textureId"); + pigeonResult.setTextureId( + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); + Object volume = map.get("volume"); + pigeonResult.setVolume((Double) volume); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PlaybackSpeedMessage { + private @NonNull Long textureId; + + public @NonNull Long getTextureId() { + return textureId; + } + + public void setTextureId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"textureId\" is null."); + } + this.textureId = setterArg; + } + + private @NonNull Double speed; + + public @NonNull Double getSpeed() { + return speed; + } + + public void setSpeed(@NonNull Double setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"speed\" is null."); + } + this.speed = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private PlaybackSpeedMessage() {} + + public static class Builder { + private @Nullable Long textureId; + + public @NonNull Builder setTextureId(@NonNull Long setterArg) { + this.textureId = setterArg; + return this; + } + + private @Nullable Double speed; + + public @NonNull Builder setSpeed(@NonNull Double setterArg) { + this.speed = setterArg; + return this; + } + + public @NonNull PlaybackSpeedMessage build() { + PlaybackSpeedMessage pigeonReturn = new PlaybackSpeedMessage(); + pigeonReturn.setTextureId(textureId); + pigeonReturn.setSpeed(speed); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + toMapResult.put("speed", speed); + return toMapResult; + } + + static @NonNull PlaybackSpeedMessage fromMap(@NonNull Map map) { + PlaybackSpeedMessage pigeonResult = new PlaybackSpeedMessage(); + Object textureId = map.get("textureId"); + pigeonResult.setTextureId( + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); + Object speed = map.get("speed"); + pigeonResult.setSpeed((Double) speed); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PositionMessage { + private @NonNull Long textureId; + + public @NonNull Long getTextureId() { + return textureId; + } + + public void setTextureId(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"textureId\" is null."); + } + this.textureId = setterArg; + } + + private @NonNull Long position; + + public @NonNull Long getPosition() { + return position; + } + + public void setPosition(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"position\" is null."); + } + this.position = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private PositionMessage() {} + + public static class Builder { + private @Nullable Long textureId; + + public @NonNull Builder setTextureId(@NonNull Long setterArg) { + this.textureId = setterArg; + return this; + } + + private @Nullable Long position; + + public @NonNull Builder setPosition(@NonNull Long setterArg) { + this.position = setterArg; + return this; + } + + public @NonNull PositionMessage build() { + PositionMessage pigeonReturn = new PositionMessage(); + pigeonReturn.setTextureId(textureId); + pigeonReturn.setPosition(position); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + toMapResult.put("position", position); + return toMapResult; + } + + static @NonNull PositionMessage fromMap(@NonNull Map map) { + PositionMessage pigeonResult = new PositionMessage(); + Object textureId = map.get("textureId"); + pigeonResult.setTextureId( + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId)); + Object position = map.get("position"); + pigeonResult.setPosition( + (position == null) + ? null + : ((position instanceof Integer) ? (Integer) position : (Long) position)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class CreateMessage { + private @Nullable String asset; + + public @Nullable String getAsset() { + return asset; + } + + public void setAsset(@Nullable String setterArg) { + this.asset = setterArg; + } + + private @Nullable String uri; + + public @Nullable String getUri() { + return uri; + } + + public void setUri(@Nullable String setterArg) { + this.uri = setterArg; + } + + private @Nullable String packageName; + + public @Nullable String getPackageName() { + return packageName; + } + + public void setPackageName(@Nullable String setterArg) { + this.packageName = setterArg; + } + + private @Nullable String formatHint; + + public @Nullable String getFormatHint() { + return formatHint; + } + + public void setFormatHint(@Nullable String setterArg) { + this.formatHint = setterArg; + } + + private @NonNull Map httpHeaders; + + public @NonNull Map getHttpHeaders() { + return httpHeaders; + } + + public void setHttpHeaders(@NonNull Map setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"httpHeaders\" is null."); + } + this.httpHeaders = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private CreateMessage() {} + + public static class Builder { + private @Nullable String asset; + + public @NonNull Builder setAsset(@Nullable String setterArg) { + this.asset = setterArg; + return this; + } + + private @Nullable String uri; + + public @NonNull Builder setUri(@Nullable String setterArg) { + this.uri = setterArg; + return this; + } + + private @Nullable String packageName; + + public @NonNull Builder setPackageName(@Nullable String setterArg) { + this.packageName = setterArg; + return this; + } + + private @Nullable String formatHint; + + public @NonNull Builder setFormatHint(@Nullable String setterArg) { + this.formatHint = setterArg; + return this; + } + + private @Nullable Map httpHeaders; + + public @NonNull Builder setHttpHeaders(@NonNull Map setterArg) { + this.httpHeaders = setterArg; + return this; + } + + public @NonNull CreateMessage build() { + CreateMessage pigeonReturn = new CreateMessage(); + pigeonReturn.setAsset(asset); + pigeonReturn.setUri(uri); + pigeonReturn.setPackageName(packageName); + pigeonReturn.setFormatHint(formatHint); + pigeonReturn.setHttpHeaders(httpHeaders); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("asset", asset); + toMapResult.put("uri", uri); + toMapResult.put("packageName", packageName); + toMapResult.put("formatHint", formatHint); + toMapResult.put("httpHeaders", httpHeaders); + return toMapResult; + } + + static @NonNull CreateMessage fromMap(@NonNull Map map) { + CreateMessage pigeonResult = new CreateMessage(); + Object asset = map.get("asset"); + pigeonResult.setAsset((String) asset); + Object uri = map.get("uri"); + pigeonResult.setUri((String) uri); + Object packageName = map.get("packageName"); + pigeonResult.setPackageName((String) packageName); + Object formatHint = map.get("formatHint"); + pigeonResult.setFormatHint((String) formatHint); + Object httpHeaders = map.get("httpHeaders"); + pigeonResult.setHttpHeaders((Map) httpHeaders); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class MixWithOthersMessage { + private @NonNull Boolean mixWithOthers; + + public @NonNull Boolean getMixWithOthers() { + return mixWithOthers; + } + + public void setMixWithOthers(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"mixWithOthers\" is null."); + } + this.mixWithOthers = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private MixWithOthersMessage() {} + + public static class Builder { + private @Nullable Boolean mixWithOthers; + + public @NonNull Builder setMixWithOthers(@NonNull Boolean setterArg) { + this.mixWithOthers = setterArg; + return this; + } + + public @NonNull MixWithOthersMessage build() { + MixWithOthersMessage pigeonReturn = new MixWithOthersMessage(); + pigeonReturn.setMixWithOthers(mixWithOthers); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("mixWithOthers", mixWithOthers); + return toMapResult; + } + + static @NonNull MixWithOthersMessage fromMap(@NonNull Map map) { + MixWithOthersMessage pigeonResult = new MixWithOthersMessage(); + Object mixWithOthers = map.get("mixWithOthers"); + pigeonResult.setMixWithOthers((Boolean) mixWithOthers); + return pigeonResult; + } + } + + private static class AndroidVideoPlayerApiCodec extends StandardMessageCodec { + public static final AndroidVideoPlayerApiCodec INSTANCE = new AndroidVideoPlayerApiCodec(); + + private AndroidVideoPlayerApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return CreateMessage.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return LoopingMessage.fromMap((Map) readValue(buffer)); + + case (byte) 130: + return MixWithOthersMessage.fromMap((Map) readValue(buffer)); + + case (byte) 131: + return PlaybackSpeedMessage.fromMap((Map) readValue(buffer)); + + case (byte) 132: + return PositionMessage.fromMap((Map) readValue(buffer)); + + case (byte) 133: + return TextureMessage.fromMap((Map) readValue(buffer)); + + case (byte) 134: + return VolumeMessage.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CreateMessage) { + stream.write(128); + writeValue(stream, ((CreateMessage) value).toMap()); + } else if (value instanceof LoopingMessage) { + stream.write(129); + writeValue(stream, ((LoopingMessage) value).toMap()); + } else if (value instanceof MixWithOthersMessage) { + stream.write(130); + writeValue(stream, ((MixWithOthersMessage) value).toMap()); + } else if (value instanceof PlaybackSpeedMessage) { + stream.write(131); + writeValue(stream, ((PlaybackSpeedMessage) value).toMap()); + } else if (value instanceof PositionMessage) { + stream.write(132); + writeValue(stream, ((PositionMessage) value).toMap()); + } else if (value instanceof TextureMessage) { + stream.write(133); + writeValue(stream, ((TextureMessage) value).toMap()); + } else if (value instanceof VolumeMessage) { + stream.write(134); + writeValue(stream, ((VolumeMessage) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface AndroidVideoPlayerApi { + void initialize(); + + @NonNull + TextureMessage create(@NonNull CreateMessage msg); + + void dispose(@NonNull TextureMessage msg); + + void setLooping(@NonNull LoopingMessage msg); + + void setVolume(@NonNull VolumeMessage msg); + + void setPlaybackSpeed(@NonNull PlaybackSpeedMessage msg); + + void play(@NonNull TextureMessage msg); + + @NonNull + PositionMessage position(@NonNull TextureMessage msg); + + void seekTo(@NonNull PositionMessage msg); + + void pause(@NonNull TextureMessage msg); + + void setMixWithOthers(@NonNull MixWithOthersMessage msg); + + /** The codec used by AndroidVideoPlayerApi. */ + static MessageCodec getCodec() { + return AndroidVideoPlayerApiCodec.INSTANCE; + } + + /** + * 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.AndroidVideoPlayerApi.initialize", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.initialize(); + 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.AndroidVideoPlayerApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + CreateMessage msgArg = (CreateMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + TextureMessage output = api.create(msgArg); + 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.AndroidVideoPlayerApi.dispose", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.dispose(msgArg); + 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.AndroidVideoPlayerApi.setLooping", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + LoopingMessage msgArg = (LoopingMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.setLooping(msgArg); + 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.AndroidVideoPlayerApi.setVolume", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + VolumeMessage msgArg = (VolumeMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.setVolume(msgArg); + 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.AndroidVideoPlayerApi.setPlaybackSpeed", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + PlaybackSpeedMessage msgArg = (PlaybackSpeedMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.setPlaybackSpeed(msgArg); + 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.AndroidVideoPlayerApi.play", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.play(msgArg); + 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.AndroidVideoPlayerApi.position", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + PositionMessage output = api.position(msgArg); + 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.AndroidVideoPlayerApi.seekTo", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + PositionMessage msgArg = (PositionMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.seekTo(msgArg); + 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.AndroidVideoPlayerApi.pause", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + TextureMessage msgArg = (TextureMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.pause(msgArg); + 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.AndroidVideoPlayerApi.setMixWithOthers", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + MixWithOthersMessage msgArg = (MixWithOthersMessage) args.get(0); + if (msgArg == null) { + throw new NullPointerException("msgArg unexpectedly null."); + } + api.setMixWithOthers(msgArg); + wrapped.put("result", null); + } 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/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java similarity index 100% rename from packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java rename to packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java similarity index 82% rename from packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java rename to packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 887d3d15f175..f215354cb929 100644 --- a/packages/video_player/video_player/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 @@ -10,14 +10,16 @@ import android.content.Context; 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; @@ -27,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; @@ -44,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; @@ -64,17 +66,17 @@ final class VideoPlayer { TextureRegistry.SurfaceTextureEntry textureEntry, String dataSource, String formatHint, - Map httpHeaders, + @NonNull Map httpHeaders, VideoPlayerOptions options) { this.eventChannel = eventChannel; 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() @@ -86,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) { @@ -132,12 +150,12 @@ private MediaSource buildMediaSource( case C.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: 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: return new HlsMediaSource.Factory(mediaDataSourceFactory) @@ -152,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 @@ -206,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); @@ -224,8 +244,7 @@ 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); } @@ -264,7 +283,8 @@ long getPosition() { } @SuppressWarnings("SuspiciousNameCombination") - private void sendInitialized() { + @VisibleForTesting + void sendInitialized() { if (isInitialized) { Map event = new HashMap<>(); event.put("event", "initialized"); @@ -282,7 +302,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/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java similarity index 100% rename from packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java rename to packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java diff --git a/packages/video_player/video_player/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 similarity index 93% rename from packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java rename to packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index d77b45e03d4b..56fabecd3a96 100644 --- a/packages/video_player/video_player/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()); @@ -156,8 +155,7 @@ public TextureMessage create(CreateMessage arg) { } videoPlayers.put(handle.id(), player); - TextureMessage result = new TextureMessage(); - result.setTextureId(handle.id()); + TextureMessage result = new TextureMessage.Builder().setTextureId(handle.id()).build(); return result; } @@ -189,8 +187,11 @@ public void play(TextureMessage arg) { public PositionMessage position(TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); - PositionMessage result = new PositionMessage(); - result.setPosition(player.getPosition()); + PositionMessage result = + new PositionMessage.Builder() + .setPosition(player.getPosition()) + .setTextureId(arg.getTextureId()) + .build(); player.sendBufferingUpdate(); return result; } @@ -239,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 new file mode 100644 index 000000000000..194f7905b63a --- /dev/null +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -0,0 +1,157 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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 { + 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 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/.gitignore b/packages/video_player/video_player_android/example/.gitignore new file mode 100644 index 000000000000..d3e68fd01e5d --- /dev/null +++ b/packages/video_player/video_player_android/example/.gitignore @@ -0,0 +1 @@ +lib/generated_plugin_registrant.dart diff --git a/packages/video_player/video_player_android/example/README.md b/packages/video_player/video_player_android/example/README.md new file mode 100644 index 000000000000..f5974e947c00 --- /dev/null +++ b/packages/video_player/video_player_android/example/README.md @@ -0,0 +1,3 @@ +# video_player_example + +Demonstrates how to use the video_player plugin. 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 new file mode 100644 index 000000000000..7b3c7db80c7e --- /dev/null +++ b/packages/video_player/video_player_android/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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 31 + + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId "io.flutter.plugins.videoplayerexample" + minSdkVersion 21 + targetSdkVersion 29 + 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' + testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'org.mockito:mockito-core:3.5.13' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/sensors/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/sensors/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java b/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..45cf5c6e9903 --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/androidTest/java/io/flutter/plugins/videoplayerexample/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.videoplayerexample; + +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/video_player/video_player_android/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/video_player_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..a2574c90d7d9 --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/packages/video_player/video_player_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/video_player/video_player_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/video_player/video_player_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/video_player/video_player_android/example/android/app/src/main/res/values/styles.xml b/packages/video_player/video_player_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..5691c756a6bc --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + + diff --git a/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml b/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000000..043e5ce55a2b --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + www.sample-videos.com + 184.72.239.149 + + \ No newline at end of file diff --git a/packages/video_player/video_player_android/example/android/app/src/test/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java b/packages/video_player/video_player_android/example/android/app/src/test/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..434861f4b754 --- /dev/null +++ b/packages/video_player/video_player_android/example/android/app/src/test/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayerexample; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugins.videoplayer.VideoPlayerPlugin; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class FlutterActivityTest { + + @Test + public void disposeAllPlayers() { + VideoPlayerPlugin videoPlayerPlugin = spy(new VideoPlayerPlugin()); + FlutterLoader flutterLoader = mock(FlutterLoader.class); + FlutterJNI flutterJNI = mock(FlutterJNI.class); + ArgumentCaptor pluginBindingCaptor = + ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); + + when(flutterJNI.isAttached()).thenReturn(true); + FlutterEngine engine = + spy(new FlutterEngine(RuntimeEnvironment.application, flutterLoader, flutterJNI)); + FlutterEngineCache.getInstance().put("my_flutter_engine", engine); + + engine.getPlugins().add(videoPlayerPlugin); + verify(videoPlayerPlugin, times(1)).onAttachedToEngine(pluginBindingCaptor.capture()); + + engine.destroy(); + verify(videoPlayerPlugin, times(1)).onDetachedFromEngine(pluginBindingCaptor.capture()); + verify(videoPlayerPlugin, times(1)).initialize(); + } +} diff --git a/packages/video_player/video_player_android/example/android/build.gradle b/packages/video_player/video_player_android/example/android/build.gradle new file mode 100644 index 000000000000..c21bff8e0a2f --- /dev/null +++ b/packages/video_player/video_player_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/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties b/packages/video_player/video_player_android/example/android/gradle.properties similarity index 100% rename from packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties rename to packages/video_player/video_player_android/example/android/gradle.properties diff --git a/packages/video_player/video_player_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..b8793d3c0d69 --- /dev/null +++ b/packages/video_player/video_player_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/share/example/android/settings.gradle b/packages/video_player/video_player_android/example/android/settings.gradle similarity index 100% rename from packages/share/example/android/settings.gradle rename to packages/video_player/video_player_android/example/android/settings.gradle diff --git a/packages/video_player/video_player_android/example/assets/Butterfly-209.mp4 b/packages/video_player/video_player_android/example/assets/Butterfly-209.mp4 new file mode 100644 index 000000000000..c8489799f549 Binary files /dev/null and b/packages/video_player/video_player_android/example/assets/Butterfly-209.mp4 differ diff --git a/packages/video_player/video_player_android/example/assets/flutter-mark-square-64.png b/packages/video_player/video_player_android/example/assets/flutter-mark-square-64.png new file mode 100644 index 000000000000..56f22d5bd8f4 Binary files /dev/null and b/packages/video_player/video_player_android/example/assets/flutter-mark-square-64.png differ 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 new file mode 100644 index 000000000000..77a618bbbdfc --- /dev/null +++ b/packages/video_player/video_player_android/example/integration_test/video_player_test.dart @@ -0,0 +1,172 @@ +// Copyright 2013 The Flutter Authors. 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 'package:flutter/services.dart' show rootBundle; +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_android/video_player_android.dart'; +// TODO(stuartmorgan): Remove the use of MiniController in tests, as that is +// testing test code; tests should instead be written directly against the +// platform interface. (These tests were copied from the app-facing package +// during federation and minimally modified, which is why they currently use the +// controller.) +import 'package:video_player_example/mini_controller.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +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. + 'cb381ced070d356799dddf24aca38ce0579d3d7b' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late MiniController _controller; + tearDown(() async => _controller.dispose()); + + group('asset videos', () { + setUp(() { + _controller = MiniController.asset(_videoAssetKey); + }); + + testWidgets('registers expected implementation', + (WidgetTester tester) async { + AndroidVideoPlayer.registerWith(); + expect(VideoPlayerPlatform.instance, isA()); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.isInitialized, true); + expect(await _controller.position, const Duration(seconds: 0)); + expect(_controller.value.duration, + const Duration(seconds: 7, milliseconds: 540)); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect( + await _controller.position, greaterThan(const Duration(seconds: 0))); + }); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.seekTo(const Duration(seconds: 3)); + + expect(await _controller.position, const Duration(seconds: 3)); + }); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + await tester.pumpAndSettle(_playDuration); + final Duration pausedPosition = (await _controller.position)!; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(await _controller.position, pausedPosition); + }); + }); + + group('file-based videos', () { + setUp(() async { + // Load the data from the asset. + final String tempDir = (await getTemporaryDirectory()).path; + final ByteData bytes = await rootBundle.load(_videoAssetKey); + + // Write it to a file to use as a source. + final String filename = _videoAssetKey.split('/').last; + final File file = File('$tempDir/$filename'); + await file.writeAsBytes(bytes.buffer.asInt8List()); + + _controller = MiniController.file(file); + }); + + testWidgets('test video player using static file() method as constructor', + (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect( + await _controller.position, greaterThan(const Duration(seconds: 0))); + }); + }); + + group('network videos', () { + setUp(() { + final String videoUrl = getUrlForAssetAsNetworkSource(_videoAssetKey); + _controller = MiniController.network(videoUrl); + }); + + testWidgets('reports buffering status', (WidgetTester tester) async { + await _controller.initialize(); + + final Completer started = Completer(); + final Completer ended = Completer(); + _controller.addListener(() { + if (!started.isCompleted && _controller.value.isBuffering) { + started.complete(); + } + if (started.isCompleted && + !_controller.value.isBuffering && + !ended.isCompleted) { + ended.complete(); + } + }); + + await _controller.play(); + await _controller.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + + expect( + await _controller.position, greaterThan(const Duration(seconds: 0))); + + await expectLater(started.future, completes); + await expectLater(ended.future, completes); + }); + + testWidgets('live stream duration != 0', (WidgetTester tester) async { + final MiniController livestreamController = MiniController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', + ); + await livestreamController.initialize(); + + expect(livestreamController.value.isInitialized, true); + // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown + // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- + expect(livestreamController.value.duration, + (Duration duration) => duration != Duration.zero); + }); + }); +} diff --git a/packages/video_player/video_player_android/example/lib/main.dart b/packages/video_player/video_player_android/example/lib/main.dart new file mode 100644 index 000000000000..bca4e291efff --- /dev/null +++ b/packages/video_player/video_player_android/example/lib/main.dart @@ -0,0 +1,234 @@ +// Copyright 2013 The Flutter 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 'mini_controller.dart'; + +void main() { + runApp( + MaterialApp( + home: _App(), + ), + ); +} + +class _App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + key: const ValueKey('home_page'), + appBar: AppBar( + title: const Text('Video player example'), + bottom: const TabBar( + isScrollable: true, + tabs: [ + Tab( + icon: Icon(Icons.cloud), + text: 'Remote', + ), + Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), + ], + ), + ), + body: TabBarView( + children: [ + _BumbleBeeRemoteVideo(), + _ButterFlyAssetVideo(), + ], + ), + ), + ); + } +} + +class _ButterFlyAssetVideo extends StatefulWidget { + @override + _ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState(); +} + +class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { + late MiniController _controller; + + @override + void initState() { + super.initState(); + _controller = MiniController.asset('assets/Butterfly-209.mp4'); + + _controller.addListener(() { + setState(() {}); + }); + _controller.initialize().then((_) => setState(() {})); + _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 assets mp4'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _BumbleBeeRemoteVideo extends StatefulWidget { + @override + _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState(); +} + +class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { + late MiniController _controller; + + @override + void initState() { + super.initState(); + _controller = MiniController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + + _controller.addListener(() { + setState(() {}); + }); + _controller.initialize(); + } + + @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 mp4'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({Key? key, required this.controller}) + : super(key: key); + + static const List _examplePlaybackRates = [ + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 5.0, + 10.0, + ]; + + final MiniController controller; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), + child: controller.value.isPlaying + ? const SizedBox.shrink() + : Container( + color: Colors.black26, + child: const Center( + child: Icon( + Icons.play_arrow, + color: Colors.white, + size: 100.0, + semanticLabel: 'Play', + ), + ), + ), + ), + GestureDetector( + onTap: () { + controller.value.isPlaying ? controller.pause() : controller.play(); + }, + ), + Align( + alignment: Alignment.topRight, + child: PopupMenuButton( + initialValue: controller.value.playbackSpeed, + tooltip: 'Playback speed', + onSelected: (double speed) { + controller.setPlaybackSpeed(speed); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final double speed in _examplePlaybackRates) + PopupMenuItem( + value: speed, + child: Text('${speed}x'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.playbackSpeed}x'), + ), + ), + ), + ], + ); + } +} 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 new file mode 100644 index 000000000000..5bce3117d0d6 --- /dev/null +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -0,0 +1,537 @@ +// Copyright 2013 The Flutter 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(stuartmorgan): Consider extracting this to a shared local (path-based) +// package for use in all implementation packages. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +VideoPlayerPlatform? _cachedPlatform; + +VideoPlayerPlatform get _platform { + if (_cachedPlatform == null) { + _cachedPlatform = VideoPlayerPlatform.instance; + _cachedPlatform!.init(); + } + return _cachedPlatform!; +} + +/// The duration, current position, buffering state, error state and settings +/// of a [MiniController]. +class VideoPlayerValue { + /// Constructs a video with the given values. Only [duration] is required. The + /// rest will initialize with default values when unset. + VideoPlayerValue({ + required this.duration, + this.size = Size.zero, + this.position = Duration.zero, + this.buffered = const [], + this.isInitialized = false, + this.isPlaying = false, + this.isBuffering = false, + this.playbackSpeed = 1.0, + this.errorDescription, + }); + + /// Returns an instance for a video that hasn't been loaded. + VideoPlayerValue.uninitialized() + : this(duration: Duration.zero, isInitialized: false); + + /// Returns an instance with the given [errorDescription]. + VideoPlayerValue.erroneous(String errorDescription) + : this( + duration: Duration.zero, + isInitialized: false, + errorDescription: errorDescription); + + /// The total duration of the video. + /// + /// The duration is [Duration.zero] if the video hasn't been initialized. + final Duration duration; + + /// The current playback position. + final Duration position; + + /// The currently buffered ranges. + final List buffered; + + /// True if the video is playing. False if it's paused. + final bool isPlaying; + + /// True if the video is currently buffering. + final bool isBuffering; + + /// The current speed of the playback. + final double playbackSpeed; + + /// A description of the error if present. + /// + /// If [hasError] is false this is `null`. + final String? errorDescription; + + /// The [size] of the currently loaded video. + final Size size; + + /// Indicates whether or not the video has been loaded and is ready to play. + final bool isInitialized; + + /// Indicates whether or not the video is in an error state. If this is true + /// [errorDescription] should have information about the problem. + bool get hasError => errorDescription != null; + + /// Returns [size.width] / [size.height]. + /// + /// Will return `1.0` if: + /// * [isInitialized] is `false` + /// * [size.width], or [size.height] is equal to `0.0` + /// * aspect ratio would be less than or equal to `0.0` + double get aspectRatio { + if (!isInitialized || size.width == 0 || size.height == 0) { + return 1.0; + } + final double aspectRatio = size.width / size.height; + if (aspectRatio <= 0) { + return 1.0; + } + return aspectRatio; + } + + /// Returns a new instance that has the same values as this current instance, + /// except for any overrides passed in as arguments to [copyWidth]. + VideoPlayerValue copyWith({ + Duration? duration, + Size? size, + Duration? position, + List? buffered, + bool? isInitialized, + bool? isPlaying, + bool? isBuffering, + double? playbackSpeed, + String? errorDescription, + }) { + return VideoPlayerValue( + duration: duration ?? this.duration, + size: size ?? this.size, + position: position ?? this.position, + buffered: buffered ?? this.buffered, + isInitialized: isInitialized ?? this.isInitialized, + isPlaying: isPlaying ?? this.isPlaying, + isBuffering: isBuffering ?? this.isBuffering, + playbackSpeed: playbackSpeed ?? this.playbackSpeed, + errorDescription: errorDescription ?? this.errorDescription, + ); + } +} + +/// A very minimal version of `VideoPlayerController` for running the example +/// without relying on `video_player`. +class MiniController extends ValueNotifier { + /// Constructs a [MiniController] playing a video from an asset. + /// + /// The name of the asset is given by the [dataSource] argument and must not be + /// null. The [package] argument must be non-null when the asset comes from a + /// package and null otherwise. + MiniController.asset(this.dataSource, {this.package}) + : dataSourceType = DataSourceType.asset, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [MiniController] playing a video from obtained from + /// the network. + MiniController.network(this.dataSource) + : dataSourceType = DataSourceType.network, + package = null, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [MiniController] playing a video from obtained from a file. + MiniController.file(File file) + : dataSource = 'file://${file.path}', + dataSourceType = DataSourceType.file, + package = null, + super(VideoPlayerValue(duration: Duration.zero)); + + /// The URI to the video file. This will be in different formats depending on + /// the [DataSourceType] of the original video. + final String dataSource; + + /// Describes the type of data source this [MiniController] + /// is constructed with. + final DataSourceType dataSourceType; + + /// Only set for [asset] videos. The package that the asset was loaded from. + final String? package; + + Timer? _timer; + Completer? _creatingCompleter; + StreamSubscription? _eventSubscription; + + /// The id of a texture that hasn't been initialized. + @visibleForTesting + static const int kUninitializedTextureId = -1; + int _textureId = kUninitializedTextureId; + + /// This is just exposed for testing. It shouldn't be used by anyone depending + /// on the plugin. + @visibleForTesting + int get textureId => _textureId; + + /// Attempts to open the given [dataSource] and load metadata about the video. + Future initialize() async { + _creatingCompleter = Completer(); + + late DataSource dataSourceDescription; + switch (dataSourceType) { + case DataSourceType.asset: + dataSourceDescription = DataSource( + sourceType: DataSourceType.asset, + asset: dataSource, + package: package, + ); + break; + case DataSourceType.network: + dataSourceDescription = DataSource( + sourceType: DataSourceType.network, + uri: dataSource, + ); + break; + case DataSourceType.file: + dataSourceDescription = DataSource( + sourceType: DataSourceType.file, + uri: dataSource, + ); + break; + case DataSourceType.contentUri: + dataSourceDescription = DataSource( + sourceType: DataSourceType.contentUri, + uri: dataSource, + ); + break; + } + + _textureId = (await _platform.create(dataSourceDescription)) ?? + kUninitializedTextureId; + _creatingCompleter!.complete(null); + final Completer initializingCompleter = Completer(); + + void eventListener(VideoEvent event) { + switch (event.eventType) { + case VideoEventType.initialized: + value = value.copyWith( + duration: event.duration, + size: event.size, + isInitialized: event.duration != null, + ); + initializingCompleter.complete(null); + _platform.setVolume(_textureId, 1.0); + _platform.setLooping(_textureId, true); + _applyPlayPause(); + break; + case VideoEventType.completed: + pause().then((void pauseResult) => seekTo(value.duration)); + break; + case VideoEventType.bufferingUpdate: + value = value.copyWith(buffered: event.buffered); + break; + case VideoEventType.bufferingStart: + value = value.copyWith(isBuffering: true); + break; + case VideoEventType.bufferingEnd: + value = value.copyWith(isBuffering: false); + break; + case VideoEventType.unknown: + break; + } + } + + void errorListener(Object obj) { + final PlatformException e = obj as PlatformException; + value = VideoPlayerValue.erroneous(e.message!); + _timer?.cancel(); + if (!initializingCompleter.isCompleted) { + initializingCompleter.completeError(obj); + } + } + + _eventSubscription = _platform + .videoEventsFor(_textureId) + .listen(eventListener, onError: errorListener); + return initializingCompleter.future; + } + + @override + Future dispose() async { + if (_creatingCompleter != null) { + await _creatingCompleter!.future; + _timer?.cancel(); + await _eventSubscription?.cancel(); + await _platform.dispose(_textureId); + } + super.dispose(); + } + + /// Starts playing the video. + Future play() async { + value = value.copyWith(isPlaying: true); + await _applyPlayPause(); + } + + /// Pauses the video. + Future pause() async { + value = value.copyWith(isPlaying: false); + await _applyPlayPause(); + } + + Future _applyPlayPause() async { + _timer?.cancel(); + if (value.isPlaying) { + await _platform.play(_textureId); + + _timer = Timer.periodic( + const Duration(milliseconds: 500), + (Timer timer) async { + final Duration? newPosition = await position; + if (newPosition == null) { + return; + } + _updatePosition(newPosition); + }, + ); + await _applyPlaybackSpeed(); + } else { + await _platform.pause(_textureId); + } + } + + Future _applyPlaybackSpeed() async { + if (value.isPlaying) { + await _platform.setPlaybackSpeed( + _textureId, + value.playbackSpeed, + ); + } + } + + /// The position in the current video. + Future get position async { + return await _platform.getPosition(_textureId); + } + + /// Sets the video's current timestamp to be at [position]. + Future seekTo(Duration position) async { + if (position > value.duration) { + position = value.duration; + } else if (position < const Duration()) { + position = const Duration(); + } + await _platform.seekTo(_textureId, position); + _updatePosition(position); + } + + /// Sets the playback speed. + Future setPlaybackSpeed(double speed) async { + value = value.copyWith(playbackSpeed: speed); + await _applyPlaybackSpeed(); + } + + void _updatePosition(Duration position) { + value = value.copyWith(position: position); + } + + @override + void removeListener(VoidCallback listener) { + super.removeListener(listener); + } +} + +/// 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, {Key? key}) : super(key: key); + + /// The [MiniController] responsible for the video being rendered in + /// this widget. + final MiniController controller; + + @override + State createState() => _VideoPlayerState(); +} + +class _VideoPlayerState extends State { + _VideoPlayerState() { + _listener = () { + final int newTextureId = widget.controller.textureId; + if (newTextureId != _textureId) { + setState(() { + _textureId = newTextureId; + }); + } + }; + } + + late VoidCallback _listener; + + late int _textureId; + + @override + void initState() { + super.initState(); + _textureId = widget.controller.textureId; + // Need to listen for initialization events since the actual texture ID + // becomes available after asynchronous initialization finishes. + widget.controller.addListener(_listener); + } + + @override + void didUpdateWidget(VideoPlayer oldWidget) { + super.didUpdateWidget(oldWidget); + oldWidget.controller.removeListener(_listener); + _textureId = widget.controller.textureId; + widget.controller.addListener(_listener); + } + + @override + void deactivate() { + super.deactivate(); + widget.controller.removeListener(_listener); + } + + @override + Widget build(BuildContext context) { + return _textureId == MiniController.kUninitializedTextureId + ? Container() + : _platform.buildView(_textureId); + } +} + +class _VideoScrubber extends StatefulWidget { + const _VideoScrubber({ + required this.child, + required this.controller, + }); + + final Widget child; + final MiniController controller; + + @override + _VideoScrubberState createState() => _VideoScrubberState(); +} + +class _VideoScrubberState extends State<_VideoScrubber> { + MiniController get controller => widget.controller; + + @override + Widget build(BuildContext context) { + void seekToRelativePosition(Offset globalPosition) { + final RenderBox box = context.findRenderObject()! as RenderBox; + final Offset tapPos = box.globalToLocal(globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = controller.value.duration * relative; + controller.seekTo(position); + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: widget.child, + onTapDown: (TapDownDetails details) { + if (controller.value.isInitialized) { + seekToRelativePosition(details.globalPosition); + } + }, + ); + } +} + +/// Displays the play/buffering status of the video controlled by [controller]. +class VideoProgressIndicator extends StatefulWidget { + /// Construct an instance that displays the play/buffering status of the video + /// controlled by [controller]. + const VideoProgressIndicator(this.controller, {Key? key}) : super(key: key); + + /// The [MiniController] that actually associates a video with this + /// widget. + final MiniController controller; + + @override + State createState() => _VideoProgressIndicatorState(); +} + +class _VideoProgressIndicatorState extends State { + _VideoProgressIndicatorState() { + listener = () { + if (mounted) { + setState(() {}); + } + }; + } + + late VoidCallback listener; + + MiniController get controller => widget.controller; + + @override + void initState() { + super.initState(); + controller.addListener(listener); + } + + @override + void deactivate() { + controller.removeListener(listener); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + const Color playedColor = Color.fromRGBO(255, 0, 0, 0.7); + const Color bufferedColor = Color.fromRGBO(50, 50, 200, 0.2); + const Color backgroundColor = Color.fromRGBO(200, 200, 200, 0.5); + + Widget progressIndicator; + if (controller.value.isInitialized) { + final int duration = controller.value.duration.inMilliseconds; + final int position = controller.value.position.inMilliseconds; + + int maxBuffering = 0; + for (final DurationRange range in controller.value.buffered) { + final int end = range.end.inMilliseconds; + if (end > maxBuffering) { + maxBuffering = end; + } + } + + progressIndicator = Stack( + fit: StackFit.passthrough, + children: [ + LinearProgressIndicator( + value: maxBuffering / duration, + valueColor: const AlwaysStoppedAnimation(bufferedColor), + backgroundColor: backgroundColor, + ), + LinearProgressIndicator( + value: position / duration, + valueColor: const AlwaysStoppedAnimation(playedColor), + backgroundColor: Colors.transparent, + ), + ], + ); + } 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, + ), + ); + } +} diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml new file mode 100644 index 000000000000..14c06f4123d4 --- /dev/null +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -0,0 +1,35 @@ +name: video_player_example +description: Demonstrates how to use the video_player plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + video_player_android: + # When depending on this package from a real application you should use: + # video_player_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: ../ + video_player_platform_interface: ">=4.2.0 <6.0.0" + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + path_provider: ^2.0.6 + test: any + +flutter: + uses-material-design: true + assets: + - assets/flutter-mark-square-64.png + - assets/Butterfly-209.mp4 diff --git a/packages/video_player/video_player_android/example/test_driver/integration_test.dart b/packages/video_player/video_player_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_android/example/test_driver/video_player.dart b/packages/video_player/video_player_android/example/test_driver/video_player.dart new file mode 100644 index 000000000000..b72354e2187f --- /dev/null +++ b/packages/video_player/video_player_android/example/test_driver/video_player.dart @@ -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 'package:flutter_driver/driver_extension.dart'; +import 'package:video_player_example/main.dart' as app; + +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart new file mode 100644 index 000000000000..cee6d7d38f66 --- /dev/null +++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart @@ -0,0 +1,186 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +import 'messages.g.dart'; + +/// An Android implementation of [VideoPlayerPlatform] that uses the +/// Pigeon-generated [VideoPlayerApi]. +class AndroidVideoPlayer extends VideoPlayerPlatform { + final AndroidVideoPlayerApi _api = AndroidVideoPlayerApi(); + + /// Registers this class as the default instance of [PathProviderPlatform]. + static void registerWith() { + VideoPlayerPlatform.instance = AndroidVideoPlayer(); + } + + @override + Future init() { + return _api.initialize(); + } + + @override + Future dispose(int textureId) { + return _api.dispose(TextureMessage(textureId: textureId)); + } + + @override + Future create(DataSource dataSource) async { + String? asset; + String? packageName; + String? uri; + String? formatHint; + Map httpHeaders = {}; + switch (dataSource.sourceType) { + case DataSourceType.asset: + asset = dataSource.asset; + packageName = dataSource.package; + break; + case DataSourceType.network: + uri = dataSource.uri; + formatHint = _videoFormatStringMap[dataSource.formatHint]; + httpHeaders = dataSource.httpHeaders; + break; + case DataSourceType.file: + uri = dataSource.uri; + break; + case DataSourceType.contentUri: + uri = dataSource.uri; + break; + } + final CreateMessage message = CreateMessage( + asset: asset, + packageName: packageName, + uri: uri, + httpHeaders: httpHeaders, + formatHint: formatHint, + ); + + 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), + rotationCorrection: map['rotationCorrection'] as int? ?? 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_android/lib/src/messages.g.dart b/packages/video_player/video_player_android/lib/src/messages.g.dart new file mode 100644 index 000000000000..0dadd2efc67e --- /dev/null +++ b/packages/video_player/video_player_android/lib/src/messages.g.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. +// 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 +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 TextureMessage { + TextureMessage({ + required this.textureId, + }); + + 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 LoopingMessage { + LoopingMessage({ + required this.textureId, + required this.isLooping, + }); + + 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 { + VolumeMessage({ + required this.textureId, + required this.volume, + }); + + 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 { + PlaybackSpeedMessage({ + required this.textureId, + required this.speed, + }); + + 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 { + PositionMessage({ + required this.textureId, + required this.position, + }); + + 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 CreateMessage { + CreateMessage({ + this.asset, + this.uri, + this.packageName, + this.formatHint, + required this.httpHeaders, + }); + + 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?)! + .cast(), + ); + } +} + +class MixWithOthersMessage { + MixWithOthersMessage({ + required this.mixWithOthers, + }); + + 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 _AndroidVideoPlayerApiCodec extends StandardMessageCodec { + const _AndroidVideoPlayerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CreateMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is LoopingMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MixWithOthersMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlaybackSpeedMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PositionMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is TextureMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CreateMessage.decode(readValue(buffer)!); + + case 129: + return LoopingMessage.decode(readValue(buffer)!); + + case 130: + return MixWithOthersMessage.decode(readValue(buffer)!); + + case 131: + return PlaybackSpeedMessage.decode(readValue(buffer)!); + + case 132: + return PositionMessage.decode(readValue(buffer)!); + + case 133: + return TextureMessage.decode(readValue(buffer)!); + + case 134: + return VolumeMessage.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +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. + AndroidVideoPlayerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _AndroidVideoPlayerApiCodec(); + + Future initialize() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.initialize', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future create(CreateMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 TextureMessage?)!; + } + } + + Future dispose(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.dispose', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setLooping(LoopingMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setLooping', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setVolume(VolumeMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setVolume', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setPlaybackSpeed(PlaybackSpeedMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setPlaybackSpeed', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 play(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.play', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 position(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.position', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 PositionMessage?)!; + } + } + + Future seekTo(PositionMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.seekTo', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 pause(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.pause', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setMixWithOthers(MixWithOthersMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.setMixWithOthers', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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/video_player/video_player_android/lib/video_player_android.dart b/packages/video_player/video_player_android/lib/video_player_android.dart new file mode 100644 index 000000000000..4e06756f1529 --- /dev/null +++ b/packages/video_player/video_player_android/lib/video_player_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_video_player.dart'; diff --git a/packages/video_player/video_player_android/pigeons/copyright.txt b/packages/video_player/video_player_android/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart new file mode 100644 index 000000000000..bf552f9369df --- /dev/null +++ b/packages/video_player/video_player_android/pigeons/messages.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/messages.g.dart', + dartTestOut: 'test/test_api.dart', + javaOut: 'android/src/main/java/io/flutter/plugins/videoplayer/Messages.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.videoplayer', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) +class TextureMessage { + TextureMessage(this.textureId); + int textureId; +} + +class LoopingMessage { + LoopingMessage(this.textureId, this.isLooping); + int textureId; + bool isLooping; +} + +class VolumeMessage { + VolumeMessage(this.textureId, this.volume); + int textureId; + double volume; +} + +class PlaybackSpeedMessage { + PlaybackSpeedMessage(this.textureId, this.speed); + int textureId; + double speed; +} + +class PositionMessage { + PositionMessage(this.textureId, this.position); + int textureId; + int position; +} + +class CreateMessage { + CreateMessage({required this.httpHeaders}); + String? asset; + String? uri; + String? packageName; + String? formatHint; + Map httpHeaders; +} + +class MixWithOthersMessage { + MixWithOthersMessage(this.mixWithOthers); + bool mixWithOthers; +} + +@HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') +abstract class AndroidVideoPlayerApi { + 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); +} diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml new file mode 100644 index 000000000000..f11a74ec1ec0 --- /dev/null +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -0,0 +1,28 @@ +name: video_player_android +description: Android implementation of the video_player plugin. +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.7 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: video_player + platforms: + android: + dartPluginClass: AndroidVideoPlayer + package: io.flutter.plugins.videoplayer + pluginClass: VideoPlayerPlugin + +dependencies: + flutter: + sdk: flutter + video_player_platform_interface: ^5.1.1 + +dev_dependencies: + flutter_test: + sdk: flutter + 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 new file mode 100644 index 000000000000..a0512a1d1d88 --- /dev/null +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -0,0 +1,364 @@ +// Copyright 2013 The Flutter 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#106316) +// ignore: unnecessary_import +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player_android/src/messages.g.dart'; +import 'package:video_player_android/video_player_android.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +import 'test_api.dart'; + +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(textureId: arg.textureId, 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(); + + test('registration', () async { + AndroidVideoPlayer.registerWith(); + expect(VideoPlayerPlatform.instance, isA()); + }); + + group('$AndroidVideoPlayer', () { + final AndroidVideoPlayer player = AndroidVideoPlayer(); + 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': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + 'rotationCorrection': 180, + }), + (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), + 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), + 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_android/test/test_api.dart b/packages/video_player/video_player_android/test/test_api.dart new file mode 100644 index 000000000000..6361522e247c --- /dev/null +++ b/packages/video_player/video_player_android/test/test_api.dart @@ -0,0 +1,303 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// 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'; + +// TODO(gaaclarke): This had to be hand tweaked from a relative path. +import 'package:video_player_android/src/messages.g.dart'; + +class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { + const _TestHostVideoPlayerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CreateMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is LoopingMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MixWithOthersMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlaybackSpeedMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PositionMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is TextureMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CreateMessage.decode(readValue(buffer)!); + + case 129: + return LoopingMessage.decode(readValue(buffer)!); + + case 130: + return MixWithOthersMessage.decode(readValue(buffer)!); + + case 131: + return PlaybackSpeedMessage.decode(readValue(buffer)!); + + case 132: + return PositionMessage.decode(readValue(buffer)!); + + case 133: + return TextureMessage.decode(readValue(buffer)!); + + case 134: + return VolumeMessage.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostVideoPlayerApi { + static const MessageCodec codec = _TestHostVideoPlayerApiCodec(); + + 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); + static void setup(TestHostVideoPlayerApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AndroidVideoPlayerApi.initialize', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.initialize(); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.create was null, expected non-null CreateMessage.'); + final TextureMessage output = api.create(arg_msg!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.dispose was null, expected non-null TextureMessage.'); + api.dispose(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); + api.setLooping(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); + api.setVolume(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); + api.setPlaybackSpeed(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.play was null, expected non-null TextureMessage.'); + api.play(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.position was null, expected non-null TextureMessage.'); + final PositionMessage output = api.position(arg_msg!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); + api.seekTo(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.pause was null, expected non-null TextureMessage.'); + api.pause(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AndroidVideoPlayerApi.setMixWithOthers was null, expected non-null MixWithOthersMessage.'); + api.setMixWithOthers(arg_msg!); + return {}; + }); + } + } + } +} diff --git a/packages/share/AUTHORS b/packages/video_player/video_player_avfoundation/AUTHORS similarity index 100% rename from packages/share/AUTHORS rename to packages/video_player/video_player_avfoundation/AUTHORS diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md new file mode 100644 index 000000000000..bb5c940d3fad --- /dev/null +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -0,0 +1,41 @@ +## NEXT + +* 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. + +## 2.2.18 + +* Wait to initialize m3u8 videos until size is set, fixing aspect ratio. +* Adjusts test timeouts for network-dependent native tests to avoid flake. + +## 2.2.17 + +* Splits from `video_player` as a federated implementation. diff --git a/packages/video_player/video_player_avfoundation/CONTRIBUTING.md b/packages/video_player/video_player_avfoundation/CONTRIBUTING.md new file mode 100644 index 000000000000..e06f2233278b --- /dev/null +++ b/packages/video_player/video_player_avfoundation/CONTRIBUTING.md @@ -0,0 +1,31 @@ +## Updating pigeon-generated files + +If you update files in the pigeons/ directory, run the following +command in this directory: + +```bash +flutter pub upgrade +flutter pub run pigeon --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 `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/LICENSE b/packages/video_player/video_player_avfoundation/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_avfoundation/README.md b/packages/video_player/video_player_avfoundation/README.md new file mode 100644 index 000000000000..97e028cf8cf5 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/README.md @@ -0,0 +1,11 @@ +# video\_player\_avfoundation + +The iOS implementation of [`video_player`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `video_player` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/video_player +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/video_player/video_player_avfoundation/example/.gitignore b/packages/video_player/video_player_avfoundation/example/.gitignore new file mode 100644 index 000000000000..d3e68fd01e5d --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/.gitignore @@ -0,0 +1 @@ +lib/generated_plugin_registrant.dart diff --git a/packages/video_player/video_player_avfoundation/example/README.md b/packages/video_player/video_player_avfoundation/example/README.md new file mode 100644 index 000000000000..f5974e947c00 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/README.md @@ -0,0 +1,3 @@ +# video_player_example + +Demonstrates how to use the video_player plugin. diff --git a/packages/video_player/video_player_avfoundation/example/assets/Butterfly-209.mp4 b/packages/video_player/video_player_avfoundation/example/assets/Butterfly-209.mp4 new file mode 100644 index 000000000000..c8489799f549 Binary files /dev/null and b/packages/video_player/video_player_avfoundation/example/assets/Butterfly-209.mp4 differ diff --git a/packages/video_player/video_player_avfoundation/example/assets/flutter-mark-square-64.png b/packages/video_player/video_player_avfoundation/example/assets/flutter-mark-square-64.png new file mode 100644 index 000000000000..56f22d5bd8f4 Binary files /dev/null and b/packages/video_player/video_player_avfoundation/example/assets/flutter-mark-square-64.png differ 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 new file mode 100644 index 000000000000..528723d092b4 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/integration_test/video_player_test.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. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart' show rootBundle; +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_avfoundation/video_player_avfoundation.dart'; +// TODO(stuartmorgan): Remove the use of MiniController in tests, as that is +// testing test code; tests should instead be written directly against the +// platform interface. (These tests were copied from the app-facing package +// during federation and minimally modified, which is why they currently use the +// controller.) +import 'package:video_player_example/mini_controller.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +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. + 'cb381ced070d356799dddf24aca38ce0579d3d7b' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late MiniController _controller; + tearDown(() async => _controller.dispose()); + + group('asset videos', () { + setUp(() { + _controller = MiniController.asset(_videoAssetKey); + }); + + testWidgets('registers expected implementation', + (WidgetTester tester) async { + AVFoundationVideoPlayer.registerWith(); + expect(VideoPlayerPlatform.instance, isA()); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.isInitialized, true); + expect(await _controller.position, const Duration(seconds: 0)); + expect(_controller.value.duration, + const Duration(seconds: 7, milliseconds: 540)); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect( + await _controller.position, greaterThan(const Duration(seconds: 0))); + }); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + + 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)); + }); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + 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)); + }); + }); + + group('file-based videos', () { + setUp(() async { + // Load the data from the asset. + final String tempDir = (await getTemporaryDirectory()).path; + final ByteData bytes = await rootBundle.load(_videoAssetKey); + + // Write it to a file to use as a source. + final String filename = _videoAssetKey.split('/').last; + final File file = File('$tempDir/$filename'); + await file.writeAsBytes(bytes.buffer.asInt8List()); + + _controller = MiniController.file(file); + }); + + testWidgets('test video player using static file() method as constructor', + (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect( + await _controller.position, greaterThan(const Duration(seconds: 0))); + }); + }); + + group('network videos', () { + setUp(() { + final String videoUrl = getUrlForAssetAsNetworkSource(_videoAssetKey); + _controller = MiniController.network(videoUrl); + }); + + testWidgets('reports buffering status', (WidgetTester tester) async { + await _controller.initialize(); + + final Completer started = Completer(); + final Completer ended = Completer(); + _controller.addListener(() { + if (!started.isCompleted && _controller.value.isBuffering) { + started.complete(); + } + if (started.isCompleted && + !_controller.value.isBuffering && + !ended.isCompleted) { + ended.complete(); + } + }); + + await _controller.play(); + await _controller.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + 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))); + + await expectLater(started.future, completes); + await expectLater(ended.future, completes); + }, + // TODO(stuartmorgan): Skipped on iOS without explanation in main + // package. Needs investigation. + skip: true); + + testWidgets('live stream duration != 0', (WidgetTester tester) async { + final MiniController livestreamController = MiniController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', + ); + await livestreamController.initialize(); + + expect(livestreamController.value.isInitialized, true); + // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown + // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- + expect(livestreamController.value.duration, + (Duration duration) => duration != Duration.zero); + }); + }); +} diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/video_player/video_player_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/local_auth/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/local_auth/example/ios/Flutter/Debug.xcconfig rename to packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig diff --git a/packages/local_auth/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/local_auth/example/ios/Flutter/Release.xcconfig rename to packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig diff --git a/packages/video_player/video_player_avfoundation/example/ios/Podfile b/packages/video_player/video_player_avfoundation/example/ios/Podfile new file mode 100644 index 000000000000..fe37427f8a74 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Podfile @@ -0,0 +1,42 @@ +# 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/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..6069bf313e8e --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,717 @@ +// !$*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 */; }; + 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 */; }; + B0F5C77B94E32FB72444AE9F /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20721C28387E1F78689EC502 /* libPods-Runner.a */; }; + D182ECB59C06DBC7E2D5D913 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BD232FD3BD3343A5F52AF50 /* libPods-RunnerTests.a */; }; + F7151F2F26603EBD0028CB91 /* VideoPlayerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */; }; + F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F7151F3126603EBD0028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F3F26603ECA0028CB91 /* 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 = ""; }; + 20721C28387E1F78689EC502 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A2EA522BDC492279A91AB75 /* 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6CDC4DA5940705A6E7671616 /* 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 = ""; }; + 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 = ""; }; + 7BD232FD3BD3343A5F52AF50 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.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 = ""; }; + B15EC39F4617FE1082B18834 /* 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 = ""; }; + C18C242FF01156F58C0DAF1C /* 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 = ""; }; + F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VideoPlayerUITests.m; sourceTree = ""; }; + F7151F3026603EBD0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VideoPlayerTests.m; sourceTree = ""; }; + F7151F3E26603ECA0028CB91 /* 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 = ( + B0F5C77B94E32FB72444AE9F /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F2926603EBD0028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F3726603ECA0028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D182ECB59C06DBC7E2D5D913 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E898481BC29A7FA83AA441 /* Pods */ = { + isa = PBXGroup; + children = ( + C18C242FF01156F58C0DAF1C /* Pods-Runner.debug.xcconfig */, + B15EC39F4617FE1082B18834 /* Pods-Runner.release.xcconfig */, + 6CDC4DA5940705A6E7671616 /* Pods-RunnerTests.debug.xcconfig */, + 2A2EA522BDC492279A91AB75 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 23104BB9DCF267F65AD246F9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 20721C28387E1F78689EC502 /* libPods-Runner.a */, + 7BD232FD3BD3343A5F52AF50 /* 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 */, + F7151F3B26603ECA0028CB91 /* RunnerTests */, + F7151F2D26603EBD0028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + 05E898481BC29A7FA83AA441 /* Pods */, + 23104BB9DCF267F65AD246F9 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */, + F7151F3A26603ECA0028CB91 /* 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 = ""; + }; + F7151F2D26603EBD0028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */, + F7151F3026603EBD0028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; + F7151F3B26603ECA0028CB91 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */, + F7151F3E26603ECA0028CB91 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + F31A669BD45D5A7C940BF077 /* [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"; + }; + F7151F2B26603EBD0028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F3526603EBD0028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F2826603EBD0028CB91 /* Sources */, + F7151F2926603EBD0028CB91 /* Frameworks */, + F7151F2A26603EBD0028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F3226603EBD0028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + F7151F3926603ECA0028CB91 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F4126603ECB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + E9F7B01F913C69934A6629F6 /* [CP] Check Pods Manifest.lock */, + F7151F3626603ECA0028CB91 /* Sources */, + F7151F3726603ECA0028CB91 /* Frameworks */, + F7151F3826603ECA0028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F4026603ECA0028CB91 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1320; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F2B26603EBD0028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + F7151F3926603ECA0028CB91 = { + 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 */, + F7151F3926603ECA0028CB91 /* RunnerTests */, + F7151F2B26603EBD0028CB91 /* 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; + }; + F7151F2A26603EBD0028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F3826603ECA0028CB91 /* 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"; + }; + 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"; + }; + E9F7B01F913C69934A6629F6 /* [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; + }; + F31A669BD45D5A7C940BF077 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F2826603EBD0028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F2F26603EBD0028CB91 /* VideoPlayerUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F3626603ECA0028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F7151F3226603EBD0028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F3126603EBD0028CB91 /* PBXContainerItemProxy */; + }; + F7151F4026603ECA0028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F3F26603ECA0028CB91 /* 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.videoPlayerExample; + 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.videoPlayerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F7151F3326603EBD0028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + 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; + }; + F7151F3426603EBD0028CB91 /* Release */ = { + isa = XCBuildConfiguration; + 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; + }; + F7151F4226603ECB0028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6CDC4DA5940705A6E7671616 /* 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"; + 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; + }; + F7151F4326603ECB0028CB91 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2A2EA522BDC492279A91AB75 /* 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"; + 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; + }; +/* 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; + }; + F7151F3526603EBD0028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F3326603EBD0028CB91 /* Debug */, + F7151F3426603EBD0028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F4126603ECB0028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F4226603ECB0028CB91 /* Debug */, + F7151F4326603ECB0028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..c5858c80e959 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/AppDelegate.h b/packages/video_player/video_player_avfoundation/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/video_player/video_player_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/sensors/example/ios/Runner/AppDelegate.m b/packages/video_player/video_player_avfoundation/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/sensors/example/ios/Runner/AppDelegate.m rename to packages/video_player/video_player_avfoundation/example/ios/Runner/AppDelegate.m diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d22f10b2ab63 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/video_player/video_player_avfoundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/video_player/video_player_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard b/packages/video_player/video_player_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..ff775ec6e32e --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist @@ -0,0 +1,54 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + video_player_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/main.m b/packages/video_player/video_player_avfoundation/example/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/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/video_player/video_player_avfoundation/example/ios/RunnerTests/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/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/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m new file mode 100644 index 000000000000..7decd04bd168 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -0,0 +1,271 @@ +// Copyright 2013 The Flutter Authors. 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 video_player_avfoundation; +@import XCTest; + +#import +#import + +@interface FLTVideoPlayer : NSObject +@property(readonly, nonatomic) AVPlayer *player; +@end + +@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)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = + [registry registrarForPlugin:@"SeekToInvokestextureFrameAvailable"]; + NSObject *partialRegistrar = OCMPartialMock(registrar); + OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry); + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:partialRegistrar]; + FLTPositionMessage *message = [FLTPositionMessage makeWithTextureId:@101 position:@0]; + FlutterError *error; + [videoPlayerPlugin seekTo:message error:&error]; + OCMVerify([mockTextureRegistry textureFrameAvailable:message.textureId.intValue]); +} + +- (void)testDeregistersFromPlayer { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = + [registry registrarForPlugin:@"testDeregistersFromPlayer"]; + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[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); + AVPlayer *avPlayer = player.player; + + [videoPlayerPlugin dispose:textureMessage error:&error]; + XCTAssertEqual(videoPlayerPlugin.playersByTextureId.count, 0); + XCTAssertNil(error); + + [self keyValueObservingExpectationForObject:avPlayer keyPath:@"currentItem" expectedValue:nil]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testVideoControls { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestVideoControls"]; + + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + NSDictionary *videoInitialization = + [self testPlugin:videoPlayerPlugin + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"]; + XCTAssertEqualObjects(videoInitialization[@"height"], @720); + XCTAssertEqualObjects(videoInitialization[@"width"], @1280); + XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); +} + +- (void)testAudioControls { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestAudioControls"]; + + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + NSDictionary *audioInitialization = + [self testPlugin:videoPlayerPlugin + uri:@"https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3"]; + XCTAssertEqualObjects(audioInitialization[@"height"], @0); + XCTAssertEqualObjects(audioInitialization[@"width"], @0); + // Perfect precision not guaranteed. + XCTAssertEqualWithAccuracy([audioInitialization[@"duration"] intValue], 5400, 200); +} + +- (void)testHLSControls { + NSObject *registry = + (NSObject *)[[UIApplication sharedApplication] delegate]; + NSObject *registrar = [registry registrarForPlugin:@"TestHLSControls"]; + + FLTVideoPlayerPlugin *videoPlayerPlugin = + (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + + NSDictionary *videoInitialization = + [self testPlugin:videoPlayerPlugin + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"]; + XCTAssertEqualObjects(videoInitialization[@"height"], @720); + XCTAssertEqualObjects(videoInitialization[@"width"], @1280); + 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; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + + FLTCreateMessage *create = [FLTCreateMessage makeWithAsset:nil + uri:uri + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error]; + + NSNumber *textureId = textureMessage.textureId; + FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureId]; + XCTAssertNotNil(player); + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; + __block NSDictionary *initializationEvent; + [player onListenWithArguments:nil + eventSink:^(NSDictionary *event) { + if ([event[@"event"] isEqualToString:@"initialized"]) { + initializationEvent = event; + XCTAssertEqual(event.count, 4); + [initializedExpectation fulfill]; + } + }]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + // Starts paused. + AVPlayer *avPlayer = player.player; + XCTAssertEqual(avPlayer.rate, 0); + XCTAssertEqual(avPlayer.volume, 1); + XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusPaused); + + // Change playback speed. + FLTPlaybackSpeedMessage *playback = [FLTPlaybackSpeedMessage makeWithTextureId:textureId + speed:@2]; + [videoPlayerPlugin setPlaybackSpeed:playback error:&error]; + XCTAssertNil(error); + XCTAssertEqual(avPlayer.rate, 2); + XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate); + + // Volume + FLTVolumeMessage *volume = [FLTVolumeMessage makeWithTextureId:textureId volume:@0.1]; + [videoPlayerPlugin setVolume:volume error:&error]; + XCTAssertNil(error); + XCTAssertEqual(avPlayer.volume, 0.1f); + + [player onCancelWithArguments:nil]; + + 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/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/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/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m new file mode 100644 index 000000000000..b9f0f16bb27b --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -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 os.log; +@import XCTest; + +@interface VideoPlayerUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication *app; +@end + +@implementation VideoPlayerUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testPlayVideo { + XCUIApplication *app = self.app; + + XCUIElement *remoteTab = [app.otherElements + elementMatchingPredicate:[NSPredicate predicateWithFormat:@"selected == YES"]]; + XCTAssertTrue([remoteTab waitForExistenceWithTimeout:30.0]); + XCTAssertTrue([remoteTab.label containsString:@"Remote"]); + + XCUIElement *playButton = app.staticTexts[@"Play"]; + XCTAssertTrue([playButton waitForExistenceWithTimeout:30.0]); + [playButton tap]; + + 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]; + + 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" ]) { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]; + XCUIElement *unselectedTab = [app.staticTexts elementMatchingPredicate:predicate]; + XCTAssertTrue([unselectedTab waitForExistenceWithTimeout:30.0]); + XCTAssertFalse(unselectedTab.isSelected); + [unselectedTab tap]; + + XCUIElement *selectedTab = [app.otherElements + elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label BEGINSWITH %@", tabName]]; + XCTAssertTrue([selectedTab waitForExistenceWithTimeout:30.0]); + XCTAssertTrue(selectedTab.isSelected); + } +} + +@end diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart new file mode 100644 index 000000000000..bca4e291efff --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -0,0 +1,234 @@ +// Copyright 2013 The Flutter 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 'mini_controller.dart'; + +void main() { + runApp( + MaterialApp( + home: _App(), + ), + ); +} + +class _App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + key: const ValueKey('home_page'), + appBar: AppBar( + title: const Text('Video player example'), + bottom: const TabBar( + isScrollable: true, + tabs: [ + Tab( + icon: Icon(Icons.cloud), + text: 'Remote', + ), + Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), + ], + ), + ), + body: TabBarView( + children: [ + _BumbleBeeRemoteVideo(), + _ButterFlyAssetVideo(), + ], + ), + ), + ); + } +} + +class _ButterFlyAssetVideo extends StatefulWidget { + @override + _ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState(); +} + +class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { + late MiniController _controller; + + @override + void initState() { + super.initState(); + _controller = MiniController.asset('assets/Butterfly-209.mp4'); + + _controller.addListener(() { + setState(() {}); + }); + _controller.initialize().then((_) => setState(() {})); + _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 assets mp4'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _BumbleBeeRemoteVideo extends StatefulWidget { + @override + _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState(); +} + +class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { + late MiniController _controller; + + @override + void initState() { + super.initState(); + _controller = MiniController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + + _controller.addListener(() { + setState(() {}); + }); + _controller.initialize(); + } + + @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 mp4'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({Key? key, required this.controller}) + : super(key: key); + + static const List _examplePlaybackRates = [ + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 5.0, + 10.0, + ]; + + final MiniController controller; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), + child: controller.value.isPlaying + ? const SizedBox.shrink() + : Container( + color: Colors.black26, + child: const Center( + child: Icon( + Icons.play_arrow, + color: Colors.white, + size: 100.0, + semanticLabel: 'Play', + ), + ), + ), + ), + GestureDetector( + onTap: () { + controller.value.isPlaying ? controller.pause() : controller.play(); + }, + ), + Align( + alignment: Alignment.topRight, + child: PopupMenuButton( + initialValue: controller.value.playbackSpeed, + tooltip: 'Playback speed', + onSelected: (double speed) { + controller.setPlaybackSpeed(speed); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final double speed in _examplePlaybackRates) + PopupMenuItem( + value: speed, + child: Text('${speed}x'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.playbackSpeed}x'), + ), + ), + ), + ], + ); + } +} 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 new file mode 100644 index 000000000000..5bce3117d0d6 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -0,0 +1,537 @@ +// Copyright 2013 The Flutter 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(stuartmorgan): Consider extracting this to a shared local (path-based) +// package for use in all implementation packages. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +VideoPlayerPlatform? _cachedPlatform; + +VideoPlayerPlatform get _platform { + if (_cachedPlatform == null) { + _cachedPlatform = VideoPlayerPlatform.instance; + _cachedPlatform!.init(); + } + return _cachedPlatform!; +} + +/// The duration, current position, buffering state, error state and settings +/// of a [MiniController]. +class VideoPlayerValue { + /// Constructs a video with the given values. Only [duration] is required. The + /// rest will initialize with default values when unset. + VideoPlayerValue({ + required this.duration, + this.size = Size.zero, + this.position = Duration.zero, + this.buffered = const [], + this.isInitialized = false, + this.isPlaying = false, + this.isBuffering = false, + this.playbackSpeed = 1.0, + this.errorDescription, + }); + + /// Returns an instance for a video that hasn't been loaded. + VideoPlayerValue.uninitialized() + : this(duration: Duration.zero, isInitialized: false); + + /// Returns an instance with the given [errorDescription]. + VideoPlayerValue.erroneous(String errorDescription) + : this( + duration: Duration.zero, + isInitialized: false, + errorDescription: errorDescription); + + /// The total duration of the video. + /// + /// The duration is [Duration.zero] if the video hasn't been initialized. + final Duration duration; + + /// The current playback position. + final Duration position; + + /// The currently buffered ranges. + final List buffered; + + /// True if the video is playing. False if it's paused. + final bool isPlaying; + + /// True if the video is currently buffering. + final bool isBuffering; + + /// The current speed of the playback. + final double playbackSpeed; + + /// A description of the error if present. + /// + /// If [hasError] is false this is `null`. + final String? errorDescription; + + /// The [size] of the currently loaded video. + final Size size; + + /// Indicates whether or not the video has been loaded and is ready to play. + final bool isInitialized; + + /// Indicates whether or not the video is in an error state. If this is true + /// [errorDescription] should have information about the problem. + bool get hasError => errorDescription != null; + + /// Returns [size.width] / [size.height]. + /// + /// Will return `1.0` if: + /// * [isInitialized] is `false` + /// * [size.width], or [size.height] is equal to `0.0` + /// * aspect ratio would be less than or equal to `0.0` + double get aspectRatio { + if (!isInitialized || size.width == 0 || size.height == 0) { + return 1.0; + } + final double aspectRatio = size.width / size.height; + if (aspectRatio <= 0) { + return 1.0; + } + return aspectRatio; + } + + /// Returns a new instance that has the same values as this current instance, + /// except for any overrides passed in as arguments to [copyWidth]. + VideoPlayerValue copyWith({ + Duration? duration, + Size? size, + Duration? position, + List? buffered, + bool? isInitialized, + bool? isPlaying, + bool? isBuffering, + double? playbackSpeed, + String? errorDescription, + }) { + return VideoPlayerValue( + duration: duration ?? this.duration, + size: size ?? this.size, + position: position ?? this.position, + buffered: buffered ?? this.buffered, + isInitialized: isInitialized ?? this.isInitialized, + isPlaying: isPlaying ?? this.isPlaying, + isBuffering: isBuffering ?? this.isBuffering, + playbackSpeed: playbackSpeed ?? this.playbackSpeed, + errorDescription: errorDescription ?? this.errorDescription, + ); + } +} + +/// A very minimal version of `VideoPlayerController` for running the example +/// without relying on `video_player`. +class MiniController extends ValueNotifier { + /// Constructs a [MiniController] playing a video from an asset. + /// + /// The name of the asset is given by the [dataSource] argument and must not be + /// null. The [package] argument must be non-null when the asset comes from a + /// package and null otherwise. + MiniController.asset(this.dataSource, {this.package}) + : dataSourceType = DataSourceType.asset, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [MiniController] playing a video from obtained from + /// the network. + MiniController.network(this.dataSource) + : dataSourceType = DataSourceType.network, + package = null, + super(VideoPlayerValue(duration: Duration.zero)); + + /// Constructs a [MiniController] playing a video from obtained from a file. + MiniController.file(File file) + : dataSource = 'file://${file.path}', + dataSourceType = DataSourceType.file, + package = null, + super(VideoPlayerValue(duration: Duration.zero)); + + /// The URI to the video file. This will be in different formats depending on + /// the [DataSourceType] of the original video. + final String dataSource; + + /// Describes the type of data source this [MiniController] + /// is constructed with. + final DataSourceType dataSourceType; + + /// Only set for [asset] videos. The package that the asset was loaded from. + final String? package; + + Timer? _timer; + Completer? _creatingCompleter; + StreamSubscription? _eventSubscription; + + /// The id of a texture that hasn't been initialized. + @visibleForTesting + static const int kUninitializedTextureId = -1; + int _textureId = kUninitializedTextureId; + + /// This is just exposed for testing. It shouldn't be used by anyone depending + /// on the plugin. + @visibleForTesting + int get textureId => _textureId; + + /// Attempts to open the given [dataSource] and load metadata about the video. + Future initialize() async { + _creatingCompleter = Completer(); + + late DataSource dataSourceDescription; + switch (dataSourceType) { + case DataSourceType.asset: + dataSourceDescription = DataSource( + sourceType: DataSourceType.asset, + asset: dataSource, + package: package, + ); + break; + case DataSourceType.network: + dataSourceDescription = DataSource( + sourceType: DataSourceType.network, + uri: dataSource, + ); + break; + case DataSourceType.file: + dataSourceDescription = DataSource( + sourceType: DataSourceType.file, + uri: dataSource, + ); + break; + case DataSourceType.contentUri: + dataSourceDescription = DataSource( + sourceType: DataSourceType.contentUri, + uri: dataSource, + ); + break; + } + + _textureId = (await _platform.create(dataSourceDescription)) ?? + kUninitializedTextureId; + _creatingCompleter!.complete(null); + final Completer initializingCompleter = Completer(); + + void eventListener(VideoEvent event) { + switch (event.eventType) { + case VideoEventType.initialized: + value = value.copyWith( + duration: event.duration, + size: event.size, + isInitialized: event.duration != null, + ); + initializingCompleter.complete(null); + _platform.setVolume(_textureId, 1.0); + _platform.setLooping(_textureId, true); + _applyPlayPause(); + break; + case VideoEventType.completed: + pause().then((void pauseResult) => seekTo(value.duration)); + break; + case VideoEventType.bufferingUpdate: + value = value.copyWith(buffered: event.buffered); + break; + case VideoEventType.bufferingStart: + value = value.copyWith(isBuffering: true); + break; + case VideoEventType.bufferingEnd: + value = value.copyWith(isBuffering: false); + break; + case VideoEventType.unknown: + break; + } + } + + void errorListener(Object obj) { + final PlatformException e = obj as PlatformException; + value = VideoPlayerValue.erroneous(e.message!); + _timer?.cancel(); + if (!initializingCompleter.isCompleted) { + initializingCompleter.completeError(obj); + } + } + + _eventSubscription = _platform + .videoEventsFor(_textureId) + .listen(eventListener, onError: errorListener); + return initializingCompleter.future; + } + + @override + Future dispose() async { + if (_creatingCompleter != null) { + await _creatingCompleter!.future; + _timer?.cancel(); + await _eventSubscription?.cancel(); + await _platform.dispose(_textureId); + } + super.dispose(); + } + + /// Starts playing the video. + Future play() async { + value = value.copyWith(isPlaying: true); + await _applyPlayPause(); + } + + /// Pauses the video. + Future pause() async { + value = value.copyWith(isPlaying: false); + await _applyPlayPause(); + } + + Future _applyPlayPause() async { + _timer?.cancel(); + if (value.isPlaying) { + await _platform.play(_textureId); + + _timer = Timer.periodic( + const Duration(milliseconds: 500), + (Timer timer) async { + final Duration? newPosition = await position; + if (newPosition == null) { + return; + } + _updatePosition(newPosition); + }, + ); + await _applyPlaybackSpeed(); + } else { + await _platform.pause(_textureId); + } + } + + Future _applyPlaybackSpeed() async { + if (value.isPlaying) { + await _platform.setPlaybackSpeed( + _textureId, + value.playbackSpeed, + ); + } + } + + /// The position in the current video. + Future get position async { + return await _platform.getPosition(_textureId); + } + + /// Sets the video's current timestamp to be at [position]. + Future seekTo(Duration position) async { + if (position > value.duration) { + position = value.duration; + } else if (position < const Duration()) { + position = const Duration(); + } + await _platform.seekTo(_textureId, position); + _updatePosition(position); + } + + /// Sets the playback speed. + Future setPlaybackSpeed(double speed) async { + value = value.copyWith(playbackSpeed: speed); + await _applyPlaybackSpeed(); + } + + void _updatePosition(Duration position) { + value = value.copyWith(position: position); + } + + @override + void removeListener(VoidCallback listener) { + super.removeListener(listener); + } +} + +/// 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, {Key? key}) : super(key: key); + + /// The [MiniController] responsible for the video being rendered in + /// this widget. + final MiniController controller; + + @override + State createState() => _VideoPlayerState(); +} + +class _VideoPlayerState extends State { + _VideoPlayerState() { + _listener = () { + final int newTextureId = widget.controller.textureId; + if (newTextureId != _textureId) { + setState(() { + _textureId = newTextureId; + }); + } + }; + } + + late VoidCallback _listener; + + late int _textureId; + + @override + void initState() { + super.initState(); + _textureId = widget.controller.textureId; + // Need to listen for initialization events since the actual texture ID + // becomes available after asynchronous initialization finishes. + widget.controller.addListener(_listener); + } + + @override + void didUpdateWidget(VideoPlayer oldWidget) { + super.didUpdateWidget(oldWidget); + oldWidget.controller.removeListener(_listener); + _textureId = widget.controller.textureId; + widget.controller.addListener(_listener); + } + + @override + void deactivate() { + super.deactivate(); + widget.controller.removeListener(_listener); + } + + @override + Widget build(BuildContext context) { + return _textureId == MiniController.kUninitializedTextureId + ? Container() + : _platform.buildView(_textureId); + } +} + +class _VideoScrubber extends StatefulWidget { + const _VideoScrubber({ + required this.child, + required this.controller, + }); + + final Widget child; + final MiniController controller; + + @override + _VideoScrubberState createState() => _VideoScrubberState(); +} + +class _VideoScrubberState extends State<_VideoScrubber> { + MiniController get controller => widget.controller; + + @override + Widget build(BuildContext context) { + void seekToRelativePosition(Offset globalPosition) { + final RenderBox box = context.findRenderObject()! as RenderBox; + final Offset tapPos = box.globalToLocal(globalPosition); + final double relative = tapPos.dx / box.size.width; + final Duration position = controller.value.duration * relative; + controller.seekTo(position); + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: widget.child, + onTapDown: (TapDownDetails details) { + if (controller.value.isInitialized) { + seekToRelativePosition(details.globalPosition); + } + }, + ); + } +} + +/// Displays the play/buffering status of the video controlled by [controller]. +class VideoProgressIndicator extends StatefulWidget { + /// Construct an instance that displays the play/buffering status of the video + /// controlled by [controller]. + const VideoProgressIndicator(this.controller, {Key? key}) : super(key: key); + + /// The [MiniController] that actually associates a video with this + /// widget. + final MiniController controller; + + @override + State createState() => _VideoProgressIndicatorState(); +} + +class _VideoProgressIndicatorState extends State { + _VideoProgressIndicatorState() { + listener = () { + if (mounted) { + setState(() {}); + } + }; + } + + late VoidCallback listener; + + MiniController get controller => widget.controller; + + @override + void initState() { + super.initState(); + controller.addListener(listener); + } + + @override + void deactivate() { + controller.removeListener(listener); + super.deactivate(); + } + + @override + Widget build(BuildContext context) { + const Color playedColor = Color.fromRGBO(255, 0, 0, 0.7); + const Color bufferedColor = Color.fromRGBO(50, 50, 200, 0.2); + const Color backgroundColor = Color.fromRGBO(200, 200, 200, 0.5); + + Widget progressIndicator; + if (controller.value.isInitialized) { + final int duration = controller.value.duration.inMilliseconds; + final int position = controller.value.position.inMilliseconds; + + int maxBuffering = 0; + for (final DurationRange range in controller.value.buffered) { + final int end = range.end.inMilliseconds; + if (end > maxBuffering) { + maxBuffering = end; + } + } + + progressIndicator = Stack( + fit: StackFit.passthrough, + children: [ + LinearProgressIndicator( + value: maxBuffering / duration, + valueColor: const AlwaysStoppedAnimation(bufferedColor), + backgroundColor: backgroundColor, + ), + LinearProgressIndicator( + value: position / duration, + valueColor: const AlwaysStoppedAnimation(playedColor), + backgroundColor: Colors.transparent, + ), + ], + ); + } 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, + ), + ); + } +} diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml new file mode 100644 index 000000000000..40b88d577ce6 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -0,0 +1,35 @@ +name: video_player_example +description: Demonstrates how to use the video_player plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + video_player_avfoundation: + # When depending on this package from a real application you should use: + # video_player_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: ../ + video_player_platform_interface: ">=4.2.0 <6.0.0" + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + path_provider: ^2.0.6 + test: any + +flutter: + uses-material-design: true + assets: + - assets/flutter-mark-square-64.png + - assets/Butterfly-209.mp4 diff --git a/packages/video_player/video_player_avfoundation/example/test_driver/integration_test.dart b/packages/video_player/video_player_avfoundation/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_avfoundation/example/test_driver/video_player.dart b/packages/video_player/video_player_avfoundation/example/test_driver/video_player.dart new file mode 100644 index 000000000000..b72354e2187f --- /dev/null +++ b/packages/video_player/video_player_avfoundation/example/test_driver/video_player.dart @@ -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 'package:flutter_driver/driver_extension.dart'; +import 'package:video_player_example/main.dart' as app; + +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/packages/package_info/ios/Assets/.gitkeep b/packages/video_player/video_player_avfoundation/ios/Assets/.gitkeep similarity index 100% rename from packages/package_info/ios/Assets/.gitkeep rename to packages/video_player/video_player_avfoundation/ios/Assets/.gitkeep 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/ios/Classes/FLTVideoPlayerPlugin.h b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.h similarity index 76% rename from packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.h rename to packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.h index 6c9d91468d6b..a737d05628f8 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.h +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.h @@ -5,4 +5,5 @@ #import @interface FLTVideoPlayerPlugin : NSObject +- (instancetype)initWithRegistrar:(NSObject *)registrar; @end diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m new file mode 100644 index 000000000000..a95779b1cbab --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -0,0 +1,637 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTVideoPlayerPlugin.h" + +#import +#import + +#import "AVAssetTrackUtils.h" +#import "messages.g.h" + +#if !__has_feature(objc_arc) +#error Code Requires ARC. +#endif + +@interface FLTFrameUpdater : NSObject +@property(nonatomic) int64_t textureId; +@property(nonatomic, weak, readonly) NSObject *registry; +- (void)onDisplayLink:(CADisplayLink *)link; +@end + +@implementation FLTFrameUpdater +- (FLTFrameUpdater *)initWithRegistry:(NSObject *)registry { + NSAssert(self, @"super init cannot be nil"); + if (self == nil) return nil; + _registry = registry; + return self; +} + +- (void)onDisplayLink:(CADisplayLink *)link { + [_registry textureFrameAvailable:_textureId]; +} +@end + +@interface FLTVideoPlayer : NSObject +@property(readonly, nonatomic) AVPlayer *player; +@property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; +@property(readonly, nonatomic) CADisplayLink *displayLink; +@property(nonatomic) FlutterEventChannel *eventChannel; +@property(nonatomic) FlutterEventSink eventSink; +@property(nonatomic) CGAffineTransform preferredTransform; +@property(nonatomic, readonly) BOOL disposed; +@property(nonatomic, readonly) BOOL isPlaying; +@property(nonatomic) BOOL isLooping; +@property(nonatomic, readonly) BOOL isInitialized; +- (instancetype)initWithURL:(NSURL *)url + frameUpdater:(FLTFrameUpdater *)frameUpdater + httpHeaders:(nonnull NSDictionary *)headers; +@end + +static void *timeRangeContext = &timeRangeContext; +static void *statusContext = &statusContext; +static void *presentationSizeContext = &presentationSizeContext; +static void *durationContext = &durationContext; +static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; +static void *playbackBufferEmptyContext = &playbackBufferEmptyContext; +static void *playbackBufferFullContext = &playbackBufferFullContext; + +@implementation FLTVideoPlayer +- (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FLTFrameUpdater *)frameUpdater { + NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; + return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:@{}]; +} + +- (void)addObservers:(AVPlayerItem *)item { + [item addObserver:self + forKeyPath:@"loadedTimeRanges" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:timeRangeContext]; + [item addObserver:self + forKeyPath:@"status" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:statusContext]; + [item addObserver:self + forKeyPath:@"presentationSize" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:presentationSizeContext]; + [item addObserver:self + forKeyPath:@"duration" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:durationContext]; + [item addObserver:self + forKeyPath:@"playbackLikelyToKeepUp" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:playbackLikelyToKeepUpContext]; + [item addObserver:self + forKeyPath:@"playbackBufferEmpty" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:playbackBufferEmptyContext]; + [item addObserver:self + forKeyPath:@"playbackBufferFull" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:playbackBufferFullContext]; + + // Add an observer that will respond to itemDidPlayToEndTime + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(itemDidPlayToEndTime:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:item]; +} + +- (void)itemDidPlayToEndTime:(NSNotification *)notification { + if (_isLooping) { + AVPlayerItem *p = [notification object]; + [p seekToTime:kCMTimeZero completionHandler:nil]; + } else { + if (_eventSink) { + _eventSink(@{@"event" : @"completed"}); + } + } +} + +const int64_t TIME_UNSET = -9223372036854775807; + +NS_INLINE int64_t FLTCMTimeToMillis(CMTime time) { + // When CMTIME_IS_INDEFINITE return a value that matches TIME_UNSET from ExoPlayer2 on Android. + // Fixes https://github.com/flutter/flutter/issues/48670 + if (CMTIME_IS_INDEFINITE(time)) return TIME_UNSET; + if (time.timescale == 0) return 0; + return time.value * 1000 / time.timescale; +} + +NS_INLINE CGFloat radiansToDegrees(CGFloat radians) { + // Input range [-pi, pi] or [-180, 180] + CGFloat degrees = GLKMathRadiansToDegrees((float)radians); + if (degrees < 0) { + // Convert -90 to 270 and -180 to 180 + return degrees + 360; + } + // Output degrees in between [0, 360] + return degrees; +}; + +- (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransform)transform + withAsset:(AVAsset *)asset + withVideoTrack:(AVAssetTrack *)videoTrack { + AVMutableVideoCompositionInstruction *instruction = + [AVMutableVideoCompositionInstruction videoCompositionInstruction]; + instruction.timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]); + AVMutableVideoCompositionLayerInstruction *layerInstruction = + [AVMutableVideoCompositionLayerInstruction + videoCompositionLayerInstructionWithAssetTrack:videoTrack]; + [layerInstruction setTransform:_preferredTransform atTime:kCMTimeZero]; + + AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition]; + instruction.layerInstructions = @[ layerInstruction ]; + videoComposition.instructions = @[ instruction ]; + + // If in portrait mode, switch the width and height of the video + CGFloat width = videoTrack.naturalSize.width; + CGFloat height = videoTrack.naturalSize.height; + NSInteger rotationDegrees = + (NSInteger)round(radiansToDegrees(atan2(_preferredTransform.b, _preferredTransform.a))); + if (rotationDegrees == 90 || rotationDegrees == 270) { + width = videoTrack.naturalSize.height; + height = videoTrack.naturalSize.width; + } + videoComposition.renderSize = CGSizeMake(width, height); + + // TODO(@recastrodiaz): should we use videoTrack.nominalFrameRate ? + // Currently set at a constant 30 FPS + videoComposition.frameDuration = CMTimeMake(1, 30); + + return videoComposition; +} + +- (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater *)frameUpdater { + NSDictionary *pixBuffAttributes = @{ + (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), + (id)kCVPixelBufferIOSurfacePropertiesKey : @{} + }; + _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes]; + + _displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater + selector:@selector(onDisplayLink:)]; + [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + _displayLink.paused = YES; +} + +- (instancetype)initWithURL:(NSURL *)url + frameUpdater:(FLTFrameUpdater *)frameUpdater + httpHeaders:(nonnull NSDictionary *)headers { + NSDictionary *options = nil; + if ([headers count] != 0) { + options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers}; + } + AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options]; + AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset]; + return [self initWithPlayerItem:item frameUpdater:frameUpdater]; +} + +- (instancetype)initWithPlayerItem:(AVPlayerItem *)item + frameUpdater:(FLTFrameUpdater *)frameUpdater { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + + AVAsset *asset = [item asset]; + void (^assetCompletionHandler)(void) = ^{ + if ([asset statusOfValueForKey:@"tracks" error:nil] == AVKeyValueStatusLoaded) { + NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; + if ([tracks count] > 0) { + AVAssetTrack *videoTrack = tracks[0]; + void (^trackCompletionHandler)(void) = ^{ + if (self->_disposed) return; + if ([videoTrack statusOfValueForKey:@"preferredTransform" + error:nil] == AVKeyValueStatusLoaded) { + // Rotate the video by using a videoComposition and the preferredTransform + 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 + // use with media served using HTTP Live Streaming. + AVMutableVideoComposition *videoComposition = + [self getVideoCompositionWithTransform:self->_preferredTransform + withAsset:asset + withVideoTrack:videoTrack]; + item.videoComposition = videoComposition; + } + }; + [videoTrack loadValuesAsynchronouslyForKeys:@[ @"preferredTransform" ] + completionHandler:trackCompletionHandler]; + } + } + }; + + _player = [AVPlayer playerWithPlayerItem:item]; + _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + + [self createVideoOutputAndDisplayLink:frameUpdater]; + + [self addObservers:item]; + + [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler]; + + return self; +} + +- (void)observeValueForKeyPath:(NSString *)path + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (context == timeRangeContext) { + if (_eventSink != nil) { + NSMutableArray *> *values = [[NSMutableArray alloc] init]; + for (NSValue *rangeValue in [object loadedTimeRanges]) { + CMTimeRange range = [rangeValue CMTimeRangeValue]; + int64_t start = FLTCMTimeToMillis(range.start); + [values addObject:@[ @(start), @(start + FLTCMTimeToMillis(range.duration)) ]]; + } + _eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); + } + } else if (context == statusContext) { + AVPlayerItem *item = (AVPlayerItem *)object; + switch (item.status) { + case AVPlayerItemStatusFailed: + if (_eventSink != nil) { + _eventSink([FlutterError + errorWithCode:@"VideoError" + message:[@"Failed to load video: " + stringByAppendingString:[item.error localizedDescription]] + details:nil]); + } + break; + case AVPlayerItemStatusUnknown: + break; + case AVPlayerItemStatusReadyToPlay: + [item addOutput:_videoOutput]; + [self setupEventSinkIfReadyToPlay]; + [self updatePlayingState]; + break; + } + } else if (context == presentationSizeContext || context == durationContext) { + AVPlayerItem *item = (AVPlayerItem *)object; + if (item.status == AVPlayerItemStatusReadyToPlay) { + // Due to an apparent bug, when the player item is ready, it still may not have determined + // its presentation size or duration. When these properties are finally set, re-check if + // all required properties and instantiate the event sink if it is not already set up. + [self setupEventSinkIfReadyToPlay]; + [self updatePlayingState]; + } + } else if (context == playbackLikelyToKeepUpContext) { + if ([[_player currentItem] isPlaybackLikelyToKeepUp]) { + [self updatePlayingState]; + if (_eventSink != nil) { + _eventSink(@{@"event" : @"bufferingEnd"}); + } + } + } else if (context == playbackBufferEmptyContext) { + if (_eventSink != nil) { + _eventSink(@{@"event" : @"bufferingStart"}); + } + } else if (context == playbackBufferFullContext) { + if (_eventSink != nil) { + _eventSink(@{@"event" : @"bufferingEnd"}); + } + } +} + +- (void)updatePlayingState { + if (!_isInitialized) { + return; + } + if (_isPlaying) { + [_player play]; + } else { + [_player pause]; + } + _displayLink.paused = !_isPlaying; +} + +- (void)setupEventSinkIfReadyToPlay { + if (_eventSink && !_isInitialized) { + AVPlayerItem *currentItem = self.player.currentItem; + CGSize size = currentItem.presentationSize; + CGFloat width = size.width; + CGFloat height = size.height; + + // Wait until tracks are loaded to check duration or if there are any videos. + AVAsset *asset = currentItem.asset; + if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) { + void (^trackCompletionHandler)(void) = ^{ + if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) { + // Cancelled, or something failed. + return; + } + // This completion block will run on an AVFoundation background queue. + // Hop back to the main thread to set up event sink. + [self performSelector:_cmd onThread:NSThread.mainThread withObject:self waitUntilDone:NO]; + }; + [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] + completionHandler:trackCompletionHandler]; + return; + } + + BOOL hasVideoTracks = [asset tracksWithMediaType:AVMediaTypeVideo].count != 0; + BOOL hasNoTracks = asset.tracks.count == 0; + + // The player has not yet initialized when it has no size, unless it is an audio-only track. + // HLS m3u8 video files never load any tracks, and are also not yet initialized until they have + // a size. + if ((hasVideoTracks || hasNoTracks) && height == CGSizeZero.height && + width == CGSizeZero.width) { + return; + } + // The player may be initialized but still needs to determine the duration. + int64_t duration = [self duration]; + if (duration == 0) { + return; + } + + _isInitialized = YES; + _eventSink(@{ + @"event" : @"initialized", + @"duration" : @(duration), + @"width" : @(width), + @"height" : @(height) + }); + } +} + +- (void)play { + _isPlaying = YES; + [self updatePlayingState]; +} + +- (void)pause { + _isPlaying = NO; + [self updatePlayingState]; +} + +- (int64_t)position { + return FLTCMTimeToMillis([_player currentTime]); +} + +- (int64_t)duration { + // Note: https://openradar.appspot.com/radar?id=4968600712511488 + // `[AVPlayerItem duration]` can be `kCMTimeIndefinite`, + // use `[[AVPlayerItem asset] duration]` instead. + return FLTCMTimeToMillis([[[_player currentItem] asset] duration]); +} + +- (void)seekTo:(int)location { + // TODO(stuartmorgan): Update this to use completionHandler: to only return + // once the seek operation is complete once the Pigeon API is updated to a + // version that handles async calls. + [_player seekToTime:CMTimeMake(location, 1000) + toleranceBefore:kCMTimeZero + toleranceAfter:kCMTimeZero]; +} + +- (void)setIsLooping:(BOOL)isLooping { + _isLooping = isLooping; +} + +- (void)setVolume:(double)volume { + _player.volume = (float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume)); +} + +- (void)setPlaybackSpeed:(double)speed { + // See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of + // these checks. + if (speed > 2.0 && !_player.currentItem.canPlayFastForward) { + if (_eventSink != nil) { + _eventSink([FlutterError errorWithCode:@"VideoError" + message:@"Video cannot be fast-forwarded beyond 2.0x" + details:nil]); + } + return; + } + + if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) { + if (_eventSink != nil) { + _eventSink([FlutterError errorWithCode:@"VideoError" + message:@"Video cannot be slow-forwarded" + details:nil]); + } + return; + } + + _player.rate = speed; +} + +- (CVPixelBufferRef)copyPixelBuffer { + CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; + if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { + return [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; + } else { + return NULL; + } +} + +- (void)onTextureUnregistered:(NSObject *)texture { + dispatch_async(dispatch_get_main_queue(), ^{ + [self dispose]; + }); +} + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + _eventSink = nil; + return nil; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + _eventSink = events; + // TODO(@recastrodiaz): remove the line below when the race condition is resolved: + // https://github.com/flutter/flutter/issues/21483 + // This line ensures the 'initialized' event is sent when the event + // 'AVPlayerItemStatusReadyToPlay' fires before _eventSink is set (this function + // onListenWithArguments is called) + [self setupEventSinkIfReadyToPlay]; + return nil; +} + +/// This method allows you to dispose without touching the event channel. This +/// is useful for the case where the Engine is in the process of deconstruction +/// so the channel is going to die or is already dead. +- (void)disposeSansEventChannel { + _disposed = YES; + [_displayLink invalidate]; + AVPlayerItem *currentItem = self.player.currentItem; + [currentItem removeObserver:self forKeyPath:@"status"]; + [currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; + [currentItem removeObserver:self forKeyPath:@"presentationSize"]; + [currentItem removeObserver:self forKeyPath:@"duration"]; + [currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; + [currentItem removeObserver:self forKeyPath:@"playbackBufferEmpty"]; + [currentItem removeObserver:self forKeyPath:@"playbackBufferFull"]; + + [self.player replaceCurrentItemWithPlayerItem:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dispose { + [self disposeSansEventChannel]; + [_eventChannel setStreamHandler:nil]; +} + +@end + +@interface FLTVideoPlayerPlugin () +@property(readonly, weak, nonatomic) NSObject *registry; +@property(readonly, weak, nonatomic) NSObject *messenger; +@property(readonly, strong, nonatomic) + NSMutableDictionary *playersByTextureId; +@property(readonly, strong, nonatomic) NSObject *registrar; +@end + +@implementation FLTVideoPlayerPlugin ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTVideoPlayerPlugin *instance = [[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + [registrar publish:instance]; + FLTAVFoundationVideoPlayerApiSetup(registrar.messenger, instance); +} + +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _registry = [registrar textures]; + _messenger = [registrar messenger]; + _registrar = registrar; + _playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1]; + return self; +} + +- (void)detachFromEngineForRegistrar:(NSObject *)registrar { + [self.playersByTextureId.allValues makeObjectsPerformSelector:@selector(disposeSansEventChannel)]; + [self.playersByTextureId removeAllObjects]; + // 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. + // FLTAVFoundationVideoPlayerApiSetup(registrar.messenger, nil); +} + +- (FLTTextureMessage *)onPlayerSetup:(FLTVideoPlayer *)player + frameUpdater:(FLTFrameUpdater *)frameUpdater { + int64_t textureId = [self.registry registerTexture:player]; + frameUpdater.textureId = textureId; + FlutterEventChannel *eventChannel = [FlutterEventChannel + eventChannelWithName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%lld", + textureId] + binaryMessenger:_messenger]; + [eventChannel setStreamHandler:player]; + player.eventChannel = eventChannel; + self.playersByTextureId[@(textureId)] = player; + FLTTextureMessage *result = [FLTTextureMessage makeWithTextureId:@(textureId)]; + return result; +} + +- (void)initialize:(FlutterError *__autoreleasing *)error { + // Allow audio playback when the Ring/Silent switch is set to silent + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; + + [self.playersByTextureId + enumerateKeysAndObjectsUsingBlock:^(NSNumber *textureId, FLTVideoPlayer *player, BOOL *stop) { + [self.registry unregisterTexture:textureId.unsignedIntegerValue]; + [player dispose]; + }]; + [self.playersByTextureId removeAllObjects]; +} + +- (FLTTextureMessage *)create:(FLTCreateMessage *)input error:(FlutterError **)error { + FLTFrameUpdater *frameUpdater = [[FLTFrameUpdater alloc] initWithRegistry:_registry]; + FLTVideoPlayer *player; + if (input.asset) { + NSString *assetPath; + if (input.packageName) { + assetPath = [_registrar lookupKeyForAsset:input.asset fromPackage:input.packageName]; + } else { + assetPath = [_registrar lookupKeyForAsset:input.asset]; + } + player = [[FLTVideoPlayer alloc] initWithAsset:assetPath frameUpdater:frameUpdater]; + return [self onPlayerSetup:player frameUpdater:frameUpdater]; + } else if (input.uri) { + player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri] + frameUpdater:frameUpdater + httpHeaders:input.httpHeaders]; + return [self onPlayerSetup:player frameUpdater:frameUpdater]; + } else { + *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; + return nil; + } +} + +- (void)dispose:(FLTTextureMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [self.registry unregisterTexture:input.textureId.intValue]; + [self.playersByTextureId removeObjectForKey:input.textureId]; + // If the Flutter contains https://github.com/flutter/engine/pull/12695, + // the `player` is disposed via `onTextureUnregistered` at the right time. + // Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the + // texture has completed the un-reregistration. It may leads a crash if we dispose the + // `player` before the texture is unregistered. We add a dispatch_after hack to make sure the + // texture is unregistered before we dispose the `player`. + // + // TODO(cyanglaz): Remove this dispatch block when + // https://github.com/flutter/flutter/commit/8159a9906095efc9af8b223f5e232cb63542ad0b is in + // stable And update the min flutter version of the plugin to the stable version. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + if (!player.disposed) { + [player dispose]; + } + }); +} + +- (void)setLooping:(FLTLoopingMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + player.isLooping = input.isLooping.boolValue; +} + +- (void)setVolume:(FLTVolumeMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player setVolume:input.volume.doubleValue]; +} + +- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player setPlaybackSpeed:input.speed.doubleValue]; +} + +- (void)play:(FLTTextureMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player play]; +} + +- (FLTPositionMessage *)position:(FLTTextureMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + FLTPositionMessage *result = [FLTPositionMessage makeWithTextureId:input.textureId + position:@([player position])]; + return result; +} + +- (void)seekTo:(FLTPositionMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player seekTo:input.position.intValue]; + [self.registry textureFrameAvailable:input.textureId.intValue]; +} + +- (void)pause:(FLTTextureMessage *)input error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player pause]; +} + +- (void)setMixWithOthers:(FLTMixWithOthersMessage *)input + error:(FlutterError *_Nullable __autoreleasing *)error { + if (input.mixWithOthers.boolValue) { + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback + withOptions:AVAudioSessionCategoryOptionMixWithOthers + error:nil]; + } else { + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; + } +} + +@end 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 new file mode 100644 index 000000000000..130d4849f372 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v2.0.1), 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 FLTTextureMessage; +@class FLTLoopingMessage; +@class FLTVolumeMessage; +@class FLTPlaybackSpeedMessage; +@class FLTPositionMessage; +@class FLTCreateMessage; +@class FLTMixWithOthersMessage; + +@interface FLTTextureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId; +@property(nonatomic, strong) NSNumber *textureId; +@end + +@interface FLTLoopingMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId isLooping:(NSNumber *)isLooping; +@property(nonatomic, strong) NSNumber *textureId; +@property(nonatomic, strong) NSNumber *isLooping; +@end + +@interface FLTVolumeMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId volume:(NSNumber *)volume; +@property(nonatomic, strong) NSNumber *textureId; +@property(nonatomic, strong) NSNumber *volume; +@end + +@interface FLTPlaybackSpeedMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId speed:(NSNumber *)speed; +@property(nonatomic, strong) NSNumber *textureId; +@property(nonatomic, strong) NSNumber *speed; +@end + +@interface FLTPositionMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId position:(NSNumber *)position; +@property(nonatomic, strong) NSNumber *textureId; +@property(nonatomic, strong) NSNumber *position; +@end + +@interface FLTCreateMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithAsset:(nullable NSString *)asset + uri:(nullable NSString *)uri + packageName:(nullable NSString *)packageName + formatHint:(nullable NSString *)formatHint + httpHeaders:(NSDictionary *)httpHeaders; +@property(nonatomic, copy, nullable) NSString *asset; +@property(nonatomic, copy, nullable) NSString *uri; +@property(nonatomic, copy, nullable) NSString *packageName; +@property(nonatomic, copy, nullable) NSString *formatHint; +@property(nonatomic, strong) NSDictionary *httpHeaders; +@end + +@interface FLTMixWithOthersMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithMixWithOthers:(NSNumber *)mixWithOthers; +@property(nonatomic, strong) NSNumber *mixWithOthers; +@end + +/// The codec used by FLTAVFoundationVideoPlayerApi. +NSObject *FLTAVFoundationVideoPlayerApiGetCodec(void); + +@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; +- (void)setLooping:(FLTLoopingMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setVolume:(FLTVolumeMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +- (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; +- (void)pause:(FLTTextureMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setMixWithOthers:(FLTMixWithOthersMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +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 new file mode 100644 index 000000000000..d82dc386878d --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m @@ -0,0 +1,544 @@ +// Copyright 2013 The Flutter 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 (v2.0.1), 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 FLTTextureMessage () ++ (FLTTextureMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTLoopingMessage () ++ (FLTLoopingMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTVolumeMessage () ++ (FLTVolumeMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTPlaybackSpeedMessage () ++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTPositionMessage () ++ (FLTPositionMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTCreateMessage () ++ (FLTCreateMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTMixWithOthersMessage () ++ (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FLTTextureMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId { + FLTTextureMessage *pigeonResult = [[FLTTextureMessage alloc] init]; + pigeonResult.textureId = textureId; + return pigeonResult; +} ++ (FLTTextureMessage *)fromMap:(NSDictionary *)dict { + FLTTextureMessage *pigeonResult = [[FLTTextureMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return + [NSDictionary dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), + @"textureId", nil]; +} +@end + +@implementation FLTLoopingMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId isLooping:(NSNumber *)isLooping { + FLTLoopingMessage *pigeonResult = [[FLTLoopingMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.isLooping = isLooping; + return pigeonResult; +} ++ (FLTLoopingMessage *)fromMap:(NSDictionary *)dict { + FLTLoopingMessage *pigeonResult = [[FLTLoopingMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + pigeonResult.isLooping = GetNullableObject(dict, @"isLooping"); + NSAssert(pigeonResult.isLooping != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.isLooping ? self.isLooping : [NSNull null]), @"isLooping", + nil]; +} +@end + +@implementation FLTVolumeMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId volume:(NSNumber *)volume { + FLTVolumeMessage *pigeonResult = [[FLTVolumeMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.volume = volume; + return pigeonResult; +} ++ (FLTVolumeMessage *)fromMap:(NSDictionary *)dict { + FLTVolumeMessage *pigeonResult = [[FLTVolumeMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + pigeonResult.volume = GetNullableObject(dict, @"volume"); + NSAssert(pigeonResult.volume != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.volume ? self.volume : [NSNull null]), @"volume", nil]; +} +@end + +@implementation FLTPlaybackSpeedMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId speed:(NSNumber *)speed { + FLTPlaybackSpeedMessage *pigeonResult = [[FLTPlaybackSpeedMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.speed = speed; + return pigeonResult; +} ++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict { + FLTPlaybackSpeedMessage *pigeonResult = [[FLTPlaybackSpeedMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + pigeonResult.speed = GetNullableObject(dict, @"speed"); + NSAssert(pigeonResult.speed != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.speed ? self.speed : [NSNull null]), @"speed", nil]; +} +@end + +@implementation FLTPositionMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId position:(NSNumber *)position { + FLTPositionMessage *pigeonResult = [[FLTPositionMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.position = position; + return pigeonResult; +} ++ (FLTPositionMessage *)fromMap:(NSDictionary *)dict { + FLTPositionMessage *pigeonResult = [[FLTPositionMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + pigeonResult.position = GetNullableObject(dict, @"position"); + NSAssert(pigeonResult.position != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.position ? self.position : [NSNull null]), @"position", + nil]; +} +@end + +@implementation FLTCreateMessage ++ (instancetype)makeWithAsset:(nullable NSString *)asset + uri:(nullable NSString *)uri + packageName:(nullable NSString *)packageName + formatHint:(nullable NSString *)formatHint + httpHeaders:(NSDictionary *)httpHeaders { + FLTCreateMessage *pigeonResult = [[FLTCreateMessage alloc] init]; + pigeonResult.asset = asset; + pigeonResult.uri = uri; + pigeonResult.packageName = packageName; + pigeonResult.formatHint = formatHint; + pigeonResult.httpHeaders = httpHeaders; + return pigeonResult; +} ++ (FLTCreateMessage *)fromMap:(NSDictionary *)dict { + FLTCreateMessage *pigeonResult = [[FLTCreateMessage alloc] init]; + pigeonResult.asset = GetNullableObject(dict, @"asset"); + pigeonResult.uri = GetNullableObject(dict, @"uri"); + pigeonResult.packageName = GetNullableObject(dict, @"packageName"); + pigeonResult.formatHint = GetNullableObject(dict, @"formatHint"); + pigeonResult.httpHeaders = GetNullableObject(dict, @"httpHeaders"); + NSAssert(pigeonResult.httpHeaders != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.asset ? self.asset : [NSNull null]), @"asset", + (self.uri ? self.uri : [NSNull null]), @"uri", + (self.packageName ? self.packageName : [NSNull null]), + @"packageName", + (self.formatHint ? self.formatHint : [NSNull null]), + @"formatHint", + (self.httpHeaders ? self.httpHeaders : [NSNull null]), + @"httpHeaders", nil]; +} +@end + +@implementation FLTMixWithOthersMessage ++ (instancetype)makeWithMixWithOthers:(NSNumber *)mixWithOthers { + FLTMixWithOthersMessage *pigeonResult = [[FLTMixWithOthersMessage alloc] init]; + pigeonResult.mixWithOthers = mixWithOthers; + return pigeonResult; +} ++ (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict { + FLTMixWithOthersMessage *pigeonResult = [[FLTMixWithOthersMessage alloc] init]; + pigeonResult.mixWithOthers = GetNullableObject(dict, @"mixWithOthers"); + NSAssert(pigeonResult.mixWithOthers != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.mixWithOthers ? self.mixWithOthers : [NSNull null]), + @"mixWithOthers", nil]; +} +@end + +@interface FLTAVFoundationVideoPlayerApiCodecReader : FlutterStandardReader +@end +@implementation FLTAVFoundationVideoPlayerApiCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [FLTCreateMessage fromMap:[self readValue]]; + + case 129: + return [FLTLoopingMessage fromMap:[self readValue]]; + + case 130: + return [FLTMixWithOthersMessage fromMap:[self readValue]]; + + case 131: + return [FLTPlaybackSpeedMessage fromMap:[self readValue]]; + + case 132: + return [FLTPositionMessage fromMap:[self readValue]]; + + case 133: + return [FLTTextureMessage fromMap:[self readValue]]; + + case 134: + return [FLTVolumeMessage fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + } +} +@end + +@interface FLTAVFoundationVideoPlayerApiCodecWriter : FlutterStandardWriter +@end +@implementation FLTAVFoundationVideoPlayerApiCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[FLTCreateMessage class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTLoopingMessage class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTMixWithOthersMessage class]]) { + [self writeByte:130]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTPlaybackSpeedMessage class]]) { + [self writeByte:131]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTPositionMessage class]]) { + [self writeByte:132]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTTextureMessage class]]) { + [self writeByte:133]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTVolumeMessage class]]) { + [self writeByte:134]; + [self writeValue:[value toMap]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface FLTAVFoundationVideoPlayerApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLTAVFoundationVideoPlayerApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLTAVFoundationVideoPlayerApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLTAVFoundationVideoPlayerApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLTAVFoundationVideoPlayerApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FLTAVFoundationVideoPlayerApiCodecReaderWriter *readerWriter = + [[FLTAVFoundationVideoPlayerApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void FLTAVFoundationVideoPlayerApiSetup(id binaryMessenger, + NSObject *api) { + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.initialize" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(initialize:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api initialize:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.create" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + FLTTextureMessage *output = [api create:arg_msg error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api dispose:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setLooping:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setVolume:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setPlaybackSpeed:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.play" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(play:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(play:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTTextureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api play:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.position" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + FLTPositionMessage *output = [api position:arg_msg error:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api seekTo:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api pause:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@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 = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setMixWithOthers:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/video_player/video_player_avfoundation/ios/video_player_avfoundation.podspec b/packages/video_player/video_player_avfoundation/ios/video_player_avfoundation.podspec new file mode 100644 index 000000000000..80dd2a53a23a --- /dev/null +++ b/packages/video_player/video_player_avfoundation/ios/video_player_avfoundation.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'video_player_avfoundation' + s.version = '0.0.1' + s.summary = 'Flutter Video Player' + s.description = <<-DESC +A Flutter plugin for playing back video on a Widget surface. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_avfoundation' } + s.documentation_url = 'https://pub.dev/packages/video_player' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end 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 new file mode 100644 index 000000000000..b5ebedda41e1 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -0,0 +1,185 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +import 'messages.g.dart'; + +/// An iOS implementation of [VideoPlayerPlatform] that uses the +/// Pigeon-generated [VideoPlayerApi]. +class AVFoundationVideoPlayer extends VideoPlayerPlatform { + final AVFoundationVideoPlayerApi _api = AVFoundationVideoPlayerApi(); + + /// Registers this class as the default instance of [VideoPlayerPlatform]. + static void registerWith() { + VideoPlayerPlatform.instance = AVFoundationVideoPlayer(); + } + + @override + Future init() { + return _api.initialize(); + } + + @override + Future dispose(int textureId) { + return _api.dispose(TextureMessage(textureId: textureId)); + } + + @override + Future create(DataSource dataSource) async { + String? asset; + String? packageName; + String? uri; + String? formatHint; + Map httpHeaders = {}; + switch (dataSource.sourceType) { + case DataSourceType.asset: + asset = dataSource.asset; + packageName = dataSource.package; + break; + case DataSourceType.network: + uri = dataSource.uri; + formatHint = _videoFormatStringMap[dataSource.formatHint]; + httpHeaders = dataSource.httpHeaders; + break; + case DataSourceType.file: + uri = dataSource.uri; + break; + case DataSourceType.contentUri: + uri = dataSource.uri; + break; + } + final CreateMessage message = CreateMessage( + asset: asset, + packageName: packageName, + uri: uri, + httpHeaders: httpHeaders, + formatHint: formatHint, + ); + + 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_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart new file mode 100644 index 000000000000..a745c66322d4 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.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. +// 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 +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 TextureMessage { + TextureMessage({ + required this.textureId, + }); + + 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 LoopingMessage { + LoopingMessage({ + required this.textureId, + required this.isLooping, + }); + + 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 { + VolumeMessage({ + required this.textureId, + required this.volume, + }); + + 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 { + PlaybackSpeedMessage({ + required this.textureId, + required this.speed, + }); + + 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 { + PositionMessage({ + required this.textureId, + required this.position, + }); + + 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 CreateMessage { + CreateMessage({ + this.asset, + this.uri, + this.packageName, + this.formatHint, + required this.httpHeaders, + }); + + 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?)! + .cast(), + ); + } +} + +class MixWithOthersMessage { + MixWithOthersMessage({ + required this.mixWithOthers, + }); + + 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 _AVFoundationVideoPlayerApiCodec extends StandardMessageCodec { + const _AVFoundationVideoPlayerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CreateMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is LoopingMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MixWithOthersMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlaybackSpeedMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PositionMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is TextureMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CreateMessage.decode(readValue(buffer)!); + + case 129: + return LoopingMessage.decode(readValue(buffer)!); + + case 130: + return MixWithOthersMessage.decode(readValue(buffer)!); + + case 131: + return PlaybackSpeedMessage.decode(readValue(buffer)!); + + case 132: + return PositionMessage.decode(readValue(buffer)!); + + case 133: + return TextureMessage.decode(readValue(buffer)!); + + case 134: + return VolumeMessage.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +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. + AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _AVFoundationVideoPlayerApiCodec(); + + Future initialize() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.initialize', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future create(CreateMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 TextureMessage?)!; + } + } + + Future dispose(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.dispose', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setLooping(LoopingMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setLooping', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setVolume(VolumeMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setVolume', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setPlaybackSpeed(PlaybackSpeedMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPlaybackSpeed', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 play(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.play', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 position(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.position', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 PositionMessage?)!; + } + } + + Future seekTo(PositionMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.seekTo', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 pause(TextureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.pause', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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 setMixWithOthers(MixWithOthersMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setMixWithOthers', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) 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/video_player/video_player_avfoundation/lib/video_player_avfoundation.dart b/packages/video_player/video_player_avfoundation/lib/video_player_avfoundation.dart new file mode 100644 index 000000000000..b36daa1b3ef8 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/lib/video_player_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_video_player.dart'; diff --git a/packages/video_player/video_player_avfoundation/pigeons/copyright.txt b/packages/video_player/video_player_avfoundation/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/video_player/video_player_avfoundation/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/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart new file mode 100644 index 000000000000..e6eda5960f29 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.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: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 TextureMessage { + TextureMessage(this.textureId); + int textureId; +} + +class LoopingMessage { + LoopingMessage(this.textureId, this.isLooping); + int textureId; + bool isLooping; +} + +class VolumeMessage { + VolumeMessage(this.textureId, this.volume); + int textureId; + double volume; +} + +class PlaybackSpeedMessage { + PlaybackSpeedMessage(this.textureId, this.speed); + int textureId; + double speed; +} + +class PositionMessage { + PositionMessage(this.textureId, this.position); + int textureId; + int position; +} + +class CreateMessage { + CreateMessage({required this.httpHeaders}); + String? asset; + String? uri; + String? packageName; + String? formatHint; + Map httpHeaders; +} + +class MixWithOthersMessage { + MixWithOthersMessage(this.mixWithOthers); + bool mixWithOthers; +} + +@HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') +abstract class AVFoundationVideoPlayerApi { + @ObjCSelector('initialize') + void initialize(); + @ObjCSelector('create:') + TextureMessage create(CreateMessage msg); + @ObjCSelector('dispose:') + void dispose(TextureMessage msg); + @ObjCSelector('setLooping:') + void setLooping(LoopingMessage msg); + @ObjCSelector('setVolume:') + void setVolume(VolumeMessage msg); + @ObjCSelector('setPlaybackSpeed:') + void setPlaybackSpeed(PlaybackSpeedMessage msg); + @ObjCSelector('play:') + void play(TextureMessage msg); + @ObjCSelector('position:') + PositionMessage position(TextureMessage msg); + @ObjCSelector('seekTo:') + void seekTo(PositionMessage msg); + @ObjCSelector('pause:') + void pause(TextureMessage msg); + @ObjCSelector('setMixWithOthers:') + void setMixWithOthers(MixWithOthersMessage msg); +} diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml new file mode 100644 index 000000000000..c00f4baa1f0a --- /dev/null +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -0,0 +1,27 @@ +name: video_player_avfoundation +description: iOS implementation of the video_player plugin. +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.5 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: video_player + platforms: + ios: + dartPluginClass: AVFoundationVideoPlayer + pluginClass: FLTVideoPlayerPlugin + +dependencies: + flutter: + sdk: flutter + video_player_platform_interface: ">=4.2.0 <6.0.0" + +dev_dependencies: + flutter_test: + sdk: flutter + 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 new file mode 100644 index 000000000000..1e6b024d9a5c --- /dev/null +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.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. + +// 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'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player_avfoundation/src/messages.g.dart'; +import 'package:video_player_avfoundation/video_player_avfoundation.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +import 'test_api.dart'; + +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(textureId: arg.textureId, 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(); + + test('registration', () async { + AVFoundationVideoPlayer.registerWith(); + expect(VideoPlayerPlatform.instance, isA()); + }); + + group('$AVFoundationVideoPlayer', () { + final AVFoundationVideoPlayer player = AVFoundationVideoPlayer(); + 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_avfoundation/test/test_api.dart b/packages/video_player/video_player_avfoundation/test/test_api.dart new file mode 100644 index 000000000000..c8f7bbd026a5 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/test/test_api.dart @@ -0,0 +1,305 @@ +// Copyright 2013 The Flutter 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 (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'; + +// TODO(gaaclarke): The following output had to be tweaked from a relative path to a uri. +import 'package:video_player_avfoundation/src/messages.g.dart'; + +class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { + const _TestHostVideoPlayerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CreateMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is LoopingMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is MixWithOthersMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is PlaybackSpeedMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PositionMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is TextureMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CreateMessage.decode(readValue(buffer)!); + + case 129: + return LoopingMessage.decode(readValue(buffer)!); + + case 130: + return MixWithOthersMessage.decode(readValue(buffer)!); + + case 131: + return PlaybackSpeedMessage.decode(readValue(buffer)!); + + case 132: + return PositionMessage.decode(readValue(buffer)!); + + case 133: + return TextureMessage.decode(readValue(buffer)!); + + case 134: + return VolumeMessage.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostVideoPlayerApi { + static const MessageCodec codec = _TestHostVideoPlayerApiCodec(); + + 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); + static void setup(TestHostVideoPlayerApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.initialize', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.initialize(); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.create was null, expected non-null CreateMessage.'); + final TextureMessage output = api.create(arg_msg!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.dispose was null, expected non-null TextureMessage.'); + api.dispose(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.setLooping was null, expected non-null LoopingMessage.'); + api.setLooping(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.setVolume was null, expected non-null VolumeMessage.'); + api.setVolume(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.setPlaybackSpeed was null, expected non-null PlaybackSpeedMessage.'); + api.setPlaybackSpeed(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.play was null, expected non-null TextureMessage.'); + api.play(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.position was null, expected non-null TextureMessage.'); + final PositionMessage output = api.position(arg_msg!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.seekTo was null, expected non-null PositionMessage.'); + api.seekTo(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.AVFoundationVideoPlayerApi.pause was null, expected non-null TextureMessage.'); + api.pause(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.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.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 b3da9c8924ef..cf5d0168efb3 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,42 @@ +## NEXT + +* 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`. + +## 5.0.2 + +* Adds the Pigeon definitions used to create the method channel implementation. +* Internal code cleanup for stricter analysis options. + +## 5.0.1 + +* Update to use the `verify` method introduced in platform_plugin_interface 2.1.0. + +## 5.0.0 + +* **BREAKING CHANGES**: + * Updates to extending `PlatformInterface`. Removes `isMock`, in favor of the + now-standard `MockPlatformInterfaceMixin`. + * Removes test.dart from the public interface. Tests in other packages should + mock `VideoPlatformInterface` rather than the method channel. + ## 4.2.0 * Add `contentUri` to `DataSourceType`. diff --git a/packages/video_player/video_player_platform_interface/CONTRIBUTING.md b/packages/video_player/video_player_platform_interface/CONTRIBUTING.md new file mode 100644 index 000000000000..4108ae0d0030 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/CONTRIBUTING.md @@ -0,0 +1,46 @@ +## 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 `main`. + +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 index 0ddbfaeaf247..831f4e3755d9 100644 --- a/packages/video_player/video_player_platform_interface/lib/messages.dart +++ b/packages/video_player/video_player_platform_interface/lib/messages.dart @@ -4,7 +4,7 @@ // 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 +// 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; diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index e01e5b8c072c..be264ca25061 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -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'; @@ -12,8 +11,12 @@ 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 { - VideoPlayerApi _api = VideoPlayerApi(); + final VideoPlayerApi _api = VideoPlayerApi(); @override Future init() { @@ -27,7 +30,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { @override Future create(DataSource dataSource) async { - CreateMessage message = CreateMessage(); + final CreateMessage message = CreateMessage(); switch (dataSource.sourceType) { case DataSourceType.asset: @@ -47,7 +50,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { break; } - TextureMessage response = await _api.create(message); + final TextureMessage response = await _api.create(message); return response.textureId; } @@ -93,7 +96,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { @override Future getPosition(int textureId) async { - PositionMessage response = + final PositionMessage response = await _api.position(TextureMessage()..textureId = textureId); return Duration(milliseconds: response.position!); } @@ -103,21 +106,22 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { return _eventChannelFor(textureId) .receiveBroadcastStream() .map((dynamic event) { - final Map map = event; + final Map map = event as Map; switch (map['event']) { case 'initialized': return VideoEvent( eventType: VideoEventType.initialized, - duration: Duration(milliseconds: map['duration']), - size: Size(map['width']?.toDouble() ?? 0.0, - map['height']?.toDouble() ?? 0.0), + 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( eventType: VideoEventType.completed, ); case 'bufferingUpdate': - final List values = map['values']; + final List values = map['values']! as List; return VideoEvent( buffered: values.map(_toDurationRange).toList(), @@ -158,10 +162,10 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { }; DurationRange _toDurationRange(dynamic value) { - final List pair = value; + final List pair = value as List; return DurationRange( - Duration(milliseconds: pair[0]), - Duration(milliseconds: pair[1]), + 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 21ad972d8e06..78173f1fb63c 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 @@ -2,12 +2,9 @@ // 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/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart' show visibleForTesting; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel_video_player.dart'; @@ -18,37 +15,24 @@ import 'method_channel_video_player.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 /// [VideoPlayerPlatform] methods. -abstract class VideoPlayerPlatform { - /// Only mock implementations should set this to true. - /// - /// Mockito mocks are implementing this class with `implements` which is forbidden for anything - /// 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 - bool get isMock => false; +abstract class VideoPlayerPlatform extends PlatformInterface { + /// Constructs a VideoPlayerPlatform. + VideoPlayerPlatform() : super(token: _token); + + static final Object _token = Object(); static VideoPlayerPlatform _instance = MethodChannelVideoPlayer(); /// The default instance of [VideoPlayerPlatform] to use. /// - /// Platform-specific plugins should override this with their own - /// platform-specific class that extends [VideoPlayerPlatform] when they - /// register themselves. - /// /// Defaults to [MethodChannelVideoPlayer]. static VideoPlayerPlatform get instance => _instance; - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 + /// Platform-specific plugins should override this with their own + /// platform-specific class that extends [VideoPlayerPlatform] when they + /// register themselves. static set instance(VideoPlayerPlatform 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; } @@ -119,14 +103,6 @@ abstract class VideoPlayerPlatform { Future setMixWithOthers(bool mixWithOthers) { throw UnimplementedError('setMixWithOthers() has not been implemented.'); } - - // This method makes sure that VideoPlayer 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() {} } /// Description of the data source used to create an instance of @@ -151,7 +127,7 @@ class DataSource { this.formatHint, this.asset, this.package, - this.httpHeaders = const {}, + this.httpHeaders = const {}, }); /// The way in which the video was originally loaded. @@ -217,17 +193,23 @@ enum VideoFormat { } /// Event emitted from the platform implementation. +@immutable class VideoEvent { /// Creates an instance of [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. + // ignore: prefer_const_constructors_in_immutables VideoEvent({ required this.eventType, this.duration, this.size, + this.rotationCorrection, this.buffered, }); @@ -244,6 +226,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]. @@ -257,15 +244,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. @@ -294,9 +284,14 @@ enum VideoEventType { /// Describes a discrete segment of time within a video using a [start] and /// [end] [Duration]. +@immutable class DurationRange { /// Trusts that the given [start] and [end] are actually in order. They should /// both be non-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. + // ignore: prefer_const_constructors_in_immutables DurationRange(this.start, this.end); /// The beginning of the segment described relative to the beginning of the @@ -337,7 +332,8 @@ class DurationRange { } @override - String toString() => '$runtimeType(start: $start, end: $end)'; + String toString() => + '${objectRuntimeType(this, 'DurationRange')}(start: $start, end: $end)'; @override bool operator ==(Object other) => @@ -348,18 +344,30 @@ 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 +@immutable class VideoPlayerOptions { + /// set additional optional player settings + // 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. + // ignore: prefer_const_constructors_in_immutables + VideoPlayerOptions({ + this.mixWithOthers = false, + this.allowBackgroundPlayback = false, + }); + + /// Set this to true to keep playing video in background, when app goes in background. + /// The default value is false. + final bool allowBackgroundPlayback; + /// Set this to true to mix the video players audio with other audio sources. /// The default value is false /// /// Note: This option will be silently ignored in the web platform (there is /// currently no way to implement this feature in this platform). final bool mixWithOthers; - - /// set additional optional player settings - VideoPlayerOptions({this.mixWithOthers = false}); } diff --git a/packages/video_player/video_player_platform_interface/pigeons/messages.dart b/packages/video_player/video_player_platform_interface/pigeons/messages.dart new file mode 100644 index 000000000000..144edb6133b0 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/pigeons/messages.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. + +// @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 35b30793a20f..8644c45b9e93 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -1,21 +1,21 @@ name: video_player_platform_interface description: A common platform interface for the video_player plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_platform_interface 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: 4.2.0 +version: 5.1.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - flutter_test: - sdk: flutter - meta: ^1.3.0 + plugin_platform_interface: ^2.1.0 dev_dependencies: - pedantic: ^1.10.0 + flutter_test: + sdk: flutter + 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 index 9da71617e66a..924dd08e464d 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -2,17 +2,20 @@ // 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'; 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/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'test.dart'; + class _ApiLogger implements TestHostVideoPlayerApi { - final List log = []; + final List log = []; TextureMessage? textureMessage; CreateMessage? createMessage; PositionMessage? positionMessage; @@ -92,10 +95,12 @@ class _ApiLogger implements TestHostVideoPlayerApi { 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(VideoPlayerPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); }); @@ -145,7 +150,7 @@ void main() { expect(log.createMessage?.uri, 'someUri'); expect(log.createMessage?.packageName, null); expect(log.createMessage?.formatHint, 'dash'); - expect(log.createMessage?.httpHeaders, {}); + expect(log.createMessage?.httpHeaders, {}); expect(textureId, 3); }); @@ -153,14 +158,15 @@ void main() { final int? textureId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', - httpHeaders: {'Authorization': 'Bearer token'}, + 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(log.createMessage?.httpHeaders, + {'Authorization': 'Bearer token'}); expect(textureId, 3); }); @@ -235,7 +241,7 @@ void main() { _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .setMockMessageHandler( - "flutter.io/videoPlayer/videoEvents123", + 'flutter.io/videoPlayer/videoEvents123', (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); @@ -243,7 +249,7 @@ void main() { await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", + 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'initialized', @@ -256,7 +262,21 @@ void main() { await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", + '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( + 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'completed', @@ -266,7 +286,7 @@ void main() { await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", + 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingUpdate', @@ -280,7 +300,7 @@ void main() { await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", + 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingStart', @@ -290,7 +310,7 @@ void main() { await _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger .handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", + 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() .encodeSuccessEnvelope({ 'event': 'bufferingEnd', @@ -312,6 +332,13 @@ 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( diff --git a/packages/video_player/video_player_platform_interface/lib/test.dart b/packages/video_player/video_player_platform_interface/test/test.dart similarity index 99% rename from packages/video_player/video_player_platform_interface/lib/test.dart rename to packages/video_player/video_player_platform_interface/test/test.dart index b4fd81f44f41..a12ae45e59db 100644 --- a/packages/video_player/video_player_platform_interface/lib/test.dart +++ b/packages/video_player/video_player_platform_interface/test/test.dart @@ -10,8 +10,7 @@ import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'messages.dart'; +import 'package:video_player_platform_interface/messages.dart'; abstract class TestHostVideoPlayerApi { void initialize(); diff --git a/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart new file mode 100644 index 000000000000..8091cd580514 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/test/video_player_options_test.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + +void main() { + test( + 'VideoPlayerOptions allowBackgroundPlayback defaults to false', + () { + final VideoPlayerOptions options = VideoPlayerOptions(); + expect(options.allowBackgroundPlayback, false); + }, + ); + test( + 'VideoPlayerOptions mixWithOthers defaults to false', + () { + final VideoPlayerOptions options = VideoPlayerOptions(); + expect(options.mixWithOthers, false); + }, + ); +} diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 4eb7c9d610b5..e36d044901a4 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,7 +1,38 @@ +## 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. + +## 2.0.6 + +* Removes dependency on `meta`. + +## 2.0.5 + +* Adds compatibility with `video_player_platform_interface` 5.0, which does not + include non-dev test dependencies. + ## 2.0.4 * Adopt `video_player_platform_interface` 4.2 and opt out of `contentUri` data source. - + ## 2.0.3 * Add `implements` to pubspec. 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..b0118514053a --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/utils.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. + +// 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'; +} 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..41aba9792e23 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart @@ -0,0 +1,195 @@ +// Copyright 2013 The Flutter Authors. 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/video_player.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); + }); + }); + }); +} 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 2a830c9c573d..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,11 +28,10 @@ void main() { .create( DataSource( sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + uri: getUrlForAssetAsNetworkSource(_videoAssetKey), ), ) - .then((textureId) => textureId!); + .then((int? textureId) => textureId!); }); testWidgets('can init', (WidgetTester tester) async { @@ -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)); }); @@ -98,14 +102,14 @@ void main() { testWidgets('throws PlatformException when playing bad media', (WidgetTester tester) async { - int videoPlayerId = (await VideoPlayerPlatform.instance.create( + 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'), + ), ))!; - Stream eventStream = + final Stream eventStream = VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); // Mute video to allow autoplay (See https://goo.gl/xX8pDD) @@ -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())); }); @@ -139,7 +143,7 @@ void main() { expect( VideoPlayerPlatform.instance.seekTo( await textureId, - Duration(seconds: 1), + const Duration(seconds: 1), ), completes, ); @@ -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 e1a38dcdcd46..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,19 +5,22 @@ 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 { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index c172eeaf1223..6fb2cd07ddf1 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -1,20 +1,20 @@ -name: connectivity_for_web_integration_tests +name: video_player_for_web_integration_tests publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.2.0" + flutter: ">=2.8.0" dependencies: - video_player_web: - path: ../ flutter: sdk: flutter + video_player_web: + path: ../ dev_dependencies: - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter diff --git a/packages/video_player/video_player_web/lib/src/shims/dart_ui.dart b/packages/video_player/video_player_web/lib/src/shims/dart_ui.dart index 5eacec5fe867..bd28793f190d 100644 --- a/packages/video_player/video_player_web/lib/src/shims/dart_ui.dart +++ b/packages/video_player/video_player_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 -// are exposed from a dedicated place. +// TODO(ditman): Remove this file once web-only dart:ui APIs are exposed from +// a dedicated place, https://github.com/flutter/flutter/issues/55000 export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; 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 f2862af8b704..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 @@ -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/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..076167383ce7 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/video_player.dart @@ -0,0 +1,254 @@ +// Copyright 2013 The Flutter Authors. 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'; + +// 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 = !_videoElement.duration.isNaN + ? Duration( + milliseconds: (_videoElement.duration * 1000).round(), + ) + : null; + + final Size? size = !_videoElement.videoHeight.isNaN + ? 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 612d22d2eb3f..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 @@ -4,35 +4,13 @@ import 'dart:async'; import 'dart:html'; -import 'src/shims/dart_ui.dart' as ui; 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'; -// 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/shims/dart_ui.dart' as ui; +import 'src/video_player.dart'; /// The web implementation of [VideoPlayerPlatform]. /// @@ -43,8 +21,10 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { VideoPlayerPlatform.instance = VideoPlayerPlugin(); } - Map _videoPlayers = {}; + // Map of textureId -> VideoPlayer instances + final Map _videoPlayers = {}; + // Simulate the native "textureId". int _textureCounter = 1; @override @@ -54,21 +34,21 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future dispose(int textureId) async { - _videoPlayers[textureId]!.dispose(); + _player(textureId).dispose(); _videoPlayers.remove(textureId); - return null; + return; } void _disposeAllPlayers() { - _videoPlayers.values - .forEach((_VideoPlayer videoPlayer) => videoPlayer.dispose()); + for (final VideoPlayer videoPlayer in _videoPlayers.values) { + videoPlayer.dispose(); + } _videoPlayers.clear(); } @override Future create(DataSource dataSource) async { - final int textureId = _textureCounter; - _textureCounter++; + final int textureId = _textureCounter++; late String uri; switch (dataSource.sourceType) { @@ -86,65 +66,76 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { uri = assetUrl; break; case DataSourceType.file: - return Future.error(UnimplementedError( + return Future.error(UnimplementedError( 'web implementation of video_player cannot play local files')); case DataSourceType.contentUri: - return Future.error(UnimplementedError( + return Future.error(UnimplementedError( '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 @@ -156,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 - 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((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 - DomException exception = e; - eventController.addError(PlatformException( - code: exception.name, - message: exception.message, - )); - }, test: (e) => e is DomException); - } - - void pause() { - videoElement.pause(); - } - - void setLooping(bool value) { - videoElement.loop = value; - } - - void setVolume(double value) { - // TODO: 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 b401673c628d..36b89abd1f31 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -1,12 +1,12 @@ name: video_player_web description: Web platform implementation of video_player. -repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web +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.4 +version: 2.0.10 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" flutter: plugin: @@ -21,10 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.3.0 - video_player_platform_interface: ^4.2.0 + video_player_platform_interface: ">=4.2.0 <6.0.0" dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 diff --git a/packages/webview_flutter/analysis_options.yaml b/packages/webview_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/webview_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/webview_flutter/AUTHORS b/packages/webview_flutter/webview_flutter/AUTHORS index 493a0b4ef9c2..85628e432f60 100644 --- a/packages/webview_flutter/webview_flutter/AUTHORS +++ b/packages/webview_flutter/webview_flutter/AUTHORS @@ -64,3 +64,5 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Nick Bradshaw +Antonino Di Natale diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 99b8a5c419ca..98ce9d263415 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,84 @@ +## NEXT + +* 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 + +* Removes a duplicate Android-specific integration test. +* Fixes an integration test race condition. +* Fixes comments (accidentally mixed // with ///). + +## 3.0.0 + +* **BREAKING CHANGE**: On Android, hybrid composition (SurfaceAndroidWebView) + is now the default. The previous default, virtual display, can be specified + with `WebView.platform = AndroidWebView()` + +## 2.8.0 + +* Adds support for the `loadFlutterAsset` method. + +## 2.7.0 + +* Adds `setCookie` to CookieManager. +* CreationParams now supports setting `initialCookies`. + +## 2.6.0 + +* Adds support for the `loadRequest` method. + +## 2.5.0 + +* Adds an option to set the background color of the webview. + +## 2.4.0 + +* Adds support for the `loadFile` and `loadHtmlString` methods. +* Updates example app Android compileSdkVersion to 31. +* Integration test fixes. +* Updates code for new analysis options. + +## 2.3.1 + +* Add iOS-specific note to set `JavascriptMode.unrestricted` in order to set `zoomEnabled: false`. + +## 2.3.0 + +* Add ability to enable/disable zoom functionality. + +## 2.2.0 + +* Added `runJavascript` and `runJavascriptForResult` to supersede `evaluateJavascript`. +* Deprecated `evaluateJavascript`. + +## 2.1.2 + +* Fix typos in the README. + +## 2.1.1 + +* Fixed `_CastError` that was thrown when running the example App. + +## 2.1.0 + +* Migrated to fully federated architecture. + ## 2.0.14 * Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. @@ -110,7 +191,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 a1a98901affb..ffe91441326d 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -7,6 +7,10 @@ A Flutter plugin that provides a WebView widget. On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). +| | Android | iOS | +|-------------|----------------|------| +| **Support** | SDK 19+ or 20+ | 9.0+ | + ## Usage Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs. @@ -15,68 +19,63 @@ You can now include a WebView widget in your widget tree. See the widget's Dartdoc for more details on how to use the widget. ## Android Platform Views -The WebView is relying on +This plugin uses [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed -the Android’s webview within the Flutter app. It supports two modes: *Virtual displays* (the current default) and *Hybrid composition*. +the Android’s webview within the Flutter app. It supports two modes: +*hybrid composition* (the current default) and *virtual display*. Here are some points to consider when choosing between the two: -* *Hybrid composition* mode has a built-in keyboard support while *Virtual displays* mode has multiple -[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22) -* *Hybrid composition* mode requires Android SKD 19+ while *Virtual displays* mode requires Android SDK 20+ -* *Hybrid composition* mode has [performence limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions +* *Hybrid composition* has built-in keyboard support while *virtual display* has multiple +[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22). +* *Hybrid composition* requires Android SDK 19+ while *virtual display* requires Android SDK 20+. +* *Hybrid composition* and *virtual display* have different + [performance tradeoffs](https://flutter.dev/docs/development/platform-integration/platform-views#performance). -| | Hybrid composition | Virtual displays | -| --------------------------- | ------------------- | ---------------- | -| **Full keyboard supoport** | yes | no | -| **Android SDK support** | 19+ | 20+ | -| **Full performance** | Android 10+ | always | -| **The default** | no | yes | -### Using Virtual displays +### Using Hybrid Composition -The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): +The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` if it was previously lower than 19: ```groovy android { defaultConfig { - minSdkVersion 20 + minSdkVersion 19 } } ``` +### Using Virtual displays -### Using Hybrid Composition - -1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 19): +1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): ```groovy android { defaultConfig { - minSdkVersion 19 + minSdkVersion 20 } } ``` -2. Set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. +2. Set `WebView.platform = AndroidWebView();` in `initState()`. For example: - + ```dart import 'dart:io'; - + import 'package:webview_flutter/webview_flutter.dart'; class WebViewExample extends StatefulWidget { @override WebViewExampleState createState() => WebViewExampleState(); } - + class WebViewExampleState extends State { @override void initState() { super.initState(); - // Enable hybrid composition. - if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + // Enable virtual display. + if (Platform.isAndroid) WebView.platform = AndroidWebView(); } @override @@ -92,3 +91,8 @@ android { To use Material Components when the user interacts with input elements in the WebView, follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). + +### 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. diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle deleted file mode 100644 index 4a164317c60f..000000000000 --- a/packages/webview_flutter/webview_flutter/android/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -group 'io.flutter.plugins.webviewflutter' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 19 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - lintOptions { - 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' - testImplementation 'androidx.test:core:1.3.0' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/settings.gradle b/packages/webview_flutter/webview_flutter/android/settings.gradle deleted file mode 100644 index 5be7a4b4c692..000000000000 --- a/packages/webview_flutter/webview_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'webview_flutter' diff --git a/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml deleted file mode 100644 index a087f2c75c24..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java deleted file mode 100644 index 31e3fe08c057..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java +++ /dev/null @@ -1,147 +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.webviewflutter; - -import static android.hardware.display.DisplayManager.DisplayListener; - -import android.annotation.TargetApi; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.util.Log; -import java.lang.reflect.Field; -import java.util.ArrayList; - -/** - * Works around an Android WebView bug by filtering some DisplayListener invocations. - * - *

Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged} - * is invoked, the display ID it is provided is of a valid display. However it turns out that when a - * display is removed Android may call onDisplayChanged with the ID of the removed display, in this - * case the Android WebView code tries to fetch and use the display with this ID and crashes with an - * NPE. - * - *

This issue was fixed in the Android WebView code in - * https://chromium-review.googlesource.com/517913 which is available starting WebView version - * 58.0.3029.125 however older webviews in the wild still have this issue. - * - *

Since Flutter removes virtual displays whenever a platform view is resized the webview crash - * is more likely to happen than other apps. And users were reporting this issue see: - * https://github.com/flutter/flutter/issues/30420 - * - *

This class works around the webview bug by unregistering the WebView's DisplayListener, and - * instead registering its own DisplayListener which delegates the callbacks to the WebView's - * listener unless it's a onDisplayChanged for an invalid display. - * - *

I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using - * reflection to fetch all registered listeners before and after initializing a webview. In the - * first initialization of a webview within the process the difference between the lists is the - * webview's display listener. - */ -@TargetApi(Build.VERSION_CODES.KITKAT) -class DisplayListenerProxy { - private static final String TAG = "DisplayListenerProxy"; - - private ArrayList listenersBeforeWebView; - - /** Should be called prior to the webview's initialization. */ - void onPreWebViewInitialization(DisplayManager displayManager) { - listenersBeforeWebView = yoinkDisplayListeners(displayManager); - } - - /** Should be called after the webview's initialization. */ - void onPostWebViewInitialization(final DisplayManager displayManager) { - final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); - // We recorded the list of listeners prior to initializing webview, any new listeners we see - // after initializing the webview are listeners added by the webview. - webViewListeners.removeAll(listenersBeforeWebView); - - if (webViewListeners.isEmpty()) { - // The Android WebView registers a single display listener per process (even if there - // are multiple WebView instances) so this list is expected to be non-empty only the - // first time a webview is initialized. - // Note that in an add2app scenario if the application had instantiated a non Flutter - // WebView prior to instantiating the Flutter WebView we are not able to get a reference - // to the WebView's display listener and can't work around the bug. - // - // This means that webview resizes in add2app Flutter apps with a non Flutter WebView - // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's - // behavior seems to be racy so it doesn't always happen). - return; - } - - for (DisplayListener webViewListener : webViewListeners) { - // Note that while DisplayManager.unregisterDisplayListener throws when given an - // unregistered listener, this isn't an issue as the WebView code never calls - // unregisterDisplayListener. - displayManager.unregisterDisplayListener(webViewListener); - - // We never explicitly unregister this listener as the webview's listener is never - // unregistered (it's released when the process is terminated). - displayManager.registerDisplayListener( - new DisplayListener() { - @Override - public void onDisplayAdded(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayAdded(displayId); - } - } - - @Override - public void onDisplayRemoved(int displayId) { - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayRemoved(displayId); - } - } - - @Override - public void onDisplayChanged(int displayId) { - if (displayManager.getDisplay(displayId) == null) { - return; - } - for (DisplayListener webViewListener : webViewListeners) { - webViewListener.onDisplayChanged(displayId); - } - } - }, - null); - } - } - - @SuppressWarnings({"unchecked", "PrivateApi"}) - private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // We cannot use reflection on Android P, but it shouldn't matter as it shipped - // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was - // fixed in 61.0.3116.0. - return new ArrayList<>(); - } - try { - Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); - displayManagerGlobalField.setAccessible(true); - Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); - Field displayListenersField = - displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); - displayListenersField.setAccessible(true); - ArrayList delegates = - (ArrayList) displayListenersField.get(displayManagerGlobal); - - Field listenerField = null; - ArrayList listeners = new ArrayList<>(); - for (Object delegate : delegates) { - if (listenerField == null) { - listenerField = delegate.getClass().getField("mListener"); - listenerField.setAccessible(true); - } - DisplayManager.DisplayListener listener = - (DisplayManager.DisplayListener) listenerField.get(delegate); - listeners.add(listener); - } - return listeners; - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.w(TAG, "Could not extract WebView's display listeners. " + e); - return new ArrayList<>(); - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index df3f21daadeb..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -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; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java deleted file mode 100644 index cfad4e315514..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java +++ /dev/null @@ -1,33 +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.webviewflutter; - -import android.webkit.DownloadListener; -import android.webkit.WebView; - -/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ -public class FlutterDownloadListener implements DownloadListener { - private final FlutterWebViewClient webViewClient; - private WebView webView; - - public FlutterDownloadListener(FlutterWebViewClient webViewClient) { - this.webViewClient = webViewClient; - } - - /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ - public void setWebView(WebView webView) { - this.webView = webView; - } - - @Override - public void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - long contentLength) { - webViewClient.notifyDownload(webView, url); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index 4651a5f5ae22..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,498 +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.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -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.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class FlutterWebView implements PlatformView, MethodCallHandler { - - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final WebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - flutterWebViewClient.onLoadingProgress(progress); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - MethodChannel methodChannel, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - - this.methodChannel = methodChannel; - this.methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - - FlutterDownloadListener flutterDownloadListener = - new FlutterDownloadListener(flutterWebViewClient); - webView = - createWebView( - new WebViewBuilder(context, containerView), - params, - new FlutterWebChromeClient(), - flutterDownloadListener); - flutterDownloadListener.setWebView(webView); - - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - - Map settings = (Map) params.get("settings"); - if (settings != null) { - applySettings(settings); - } - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) { - registerJavaScriptChannelNames(names); - } - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) { - updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - } - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - /** - * Creates a {@link android.webkit.WebView} and configures it according to the supplied - * parameters. - * - *

The {@link WebView} is configured with the following predefined settings: - * - *

    - *
  • always enable the DOM storage API; - *
  • always allow JavaScript to automatically open windows; - *
  • always allow support for multiple windows; - *
  • always use the {@link FlutterWebChromeClient} as web Chrome client. - *
- * - *

Important: This method is visible for testing purposes only and should - * never be called from outside this class. - * - * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link - * WebView}. - * @param params creation parameters received over the method channel. - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return The new {@link android.webkit.WebView} object. - */ - @VisibleForTesting - static WebView createWebView( - WebViewBuilder webViewBuilder, - Map params, - WebChromeClient webChromeClient, - @Nullable DownloadListener downloadListener) { - boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); - webViewBuilder - .setUsesHybridComposition(usesHybridComposition) - .setDomStorageEnabled(true) // Always enable DOM storage API. - .setJavaScriptCanOpenWindowsAutomatically( - true) // Always allow automatically opening of windows. - .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient(webChromeClient) - .setDownloadListener( - downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. - - return webViewBuilder.build(); - } - - @Override - public View getView() { - return webView; - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionUnlocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).unlockInputConnection(); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionLocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).lockInputConnection(); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewAttached(View flutterView) { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(flutterView); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewDetached() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(null); - } - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) { - updateJsMode(mode); - } - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "hasProgressTracking": - flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "allowsInlineMediaPlayback": - // no-op inline media playback is always allowed on Android. - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).dispose(); - } - webView.destroy(); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index 260ef8e8b15d..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - boolean hasProgressTracking; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - /** - * Notifies the Flutter code that a download should start when a navigation delegate is set. - * - * @param view the webView the result of the navigation delegate will be send to. - * @param url the download url - * @return A boolean whether or not the request is forwarded to the Flutter code. - */ - boolean notifyDownload(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - void onLoadingProgress(int progress) { - if (hasProgressTracking) { - Map args = new HashMap<>(); - args.put("progress", progress); - methodChannel.invokeMethod("onProgress", args); - } - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java deleted file mode 100644 index 8fe58104a0fb..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java +++ /dev/null @@ -1,33 +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.webviewflutter; - -import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.StandardMessageCodec; -import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; - -public final class FlutterWebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; - - FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { - super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; - } - - @SuppressWarnings("unchecked") - @Override - public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - return new FlutterWebView(context, methodChannel, params, containerView); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java deleted file mode 100644 index 51b2a3809fff..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Build; -import android.util.Log; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.ListPopupWindow; - -/** - * A WebView subclass that mirrors the same implementation hacks that the system WebView does in - * order to correctly create an InputConnection. - * - *

These hacks are only needed in Android versions below N and exist to create an InputConnection - * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in - * {@link #checkInputConnectionProxy}. - * - *

See also {@link ThreadedInputConnectionProxyAdapterView}. - */ -final class InputAwareWebView extends WebView { - private static final String TAG = "InputAwareWebView"; - private View threadedInputConnectionProxyView; - private ThreadedInputConnectionProxyAdapterView proxyAdapterView; - private View containerView; - - InputAwareWebView(Context context, View containerView) { - super(context); - this.containerView = containerView; - } - - void setContainerView(View containerView) { - this.containerView = containerView; - - if (proxyAdapterView == null) { - return; - } - - Log.w(TAG, "The containerView has changed while the proxyAdapterView exists."); - if (containerView != null) { - setInputConnectionTarget(proxyAdapterView); - } - } - - /** - * Set our proxy adapter view to use its cached input connection instead of creating new ones. - * - *

This is used to avoid losing our input connection when the virtual display is resized. - */ - void lockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(true); - } - - /** Sets the proxy adapter view back to its default behavior. */ - void unlockInputConnection() { - if (proxyAdapterView == null) { - return; - } - - proxyAdapterView.setLocked(false); - } - - /** Restore the original InputConnection, if needed. */ - void dispose() { - resetInputConnection(); - } - - /** - * Creates an InputConnection from the IME thread when needed. - * - *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an - * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the - * system calling this method for WebView's proxy view in order to know when we need to create our - * own. - * - *

This method would normally be called for any View that used the InputMethodManager. We rely - * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the - * system WebView in order to know whether or not the system WebView expects an InputConnection on - * the IME thread. - */ - @Override - public boolean checkInputConnectionProxy(final View view) { - // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. - View previousProxy = threadedInputConnectionProxyView; - threadedInputConnectionProxyView = view; - if (previousProxy == view) { - // This isn't a new ThreadedInputConnectionProxyView. Ignore it. - return super.checkInputConnectionProxy(view); - } - if (containerView == null) { - Log.e( - TAG, - "Can't create a proxy view because there's no container view. Text input may not work."); - return super.checkInputConnectionProxy(view); - } - - // We've never seen this before, so we make the assumption that this is WebView's - // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could - // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. - proxyAdapterView = - new ThreadedInputConnectionProxyAdapterView( - /*containerView=*/ containerView, - /*targetView=*/ view, - /*imeHandler=*/ view.getHandler()); - setInputConnectionTarget(/*targetView=*/ proxyAdapterView); - return super.checkInputConnectionProxy(view); - } - - /** - * Ensure that input creation happens back on {@link #containerView}'s thread once this view no - * longer has focus. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - @Override - public void clearFocus() { - super.clearFocus(); - resetInputConnection(); - } - - /** - * Ensure that input creation happens back on {@link #containerView}. - * - *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's - * thread for all connections. We undo it here so users will be able to go back to typing in - * Flutter UIs as expected. - */ - private void resetInputConnection() { - if (proxyAdapterView == null) { - // No need to reset the InputConnection to the default thread if we've never changed it. - return; - } - if (containerView == null) { - Log.e(TAG, "Can't reset the input connection to the container view because there is none."); - return; - } - setInputConnectionTarget(/*targetView=*/ containerView); - } - - /** - * This is the crucial trick that gets the InputConnection creation to happen on the correct - * thread pre Android N. - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a - * - *

{@code targetView} should have a {@link View#getHandler} method with the thread that future - * InputConnections should be created on. - */ - private void setInputConnectionTarget(final View targetView) { - if (containerView == null) { - Log.e( - TAG, - "Can't set the input connection target because there is no containerView to use as a handler."); - return; - } - - targetView.requestFocus(); - containerView.post( - new Runnable() { - @Override - public void run() { - InputMethodManager imm = - (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); - // This is a hack to make InputMethodManager believe that the target view now has focus. - // As a result, InputMethodManager will think that targetView is focused, and will call - // getHandler() of the view when creating input connection. - - // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect - // the real window focus. - targetView.onWindowFocusChanged(true); - - // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call - // onCreateInputConnection() on targetView on the same thread as - // targetView.getHandler(). It will also call subsequent InputConnection methods on this - // thread. This is the IME thread in cases where targetView is our proxyAdapterView. - imm.isActive(containerView); - } - }); - } - - @Override - protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - // This works around a crash when old (<67.0.3367.0) Chromium versions are used. - - // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown - // on tablets: - // - // - WebView is calling ListPopupWindow#show - // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. - // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is - // also synchronously performing the following sequence: - // - WebView's focus change listener is loosing focus (as mDropDownList got it) - // - WebView is hiding all popups (as it lost focus) - // - WebView's SelectPopupDropDown#hide is invoked. - // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. - // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). - // - // To workaround this, we drop the problematic focus lost call. - // See more details on: https://github.com/flutter/flutter/issues/54164 - // - // We don't do this after Android P as it shipped with a new enough WebView version, and it's - // better to not do this on all future Android versions in case DropDownListView's code changes. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && isCalledFromListPopupWindowShow() - && !focused) { - return; - } - super.onFocusChanged(focused, direction, previouslyFocusedRect); - } - - private boolean isCalledFromListPopupWindowShow() { - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (StackTraceElement stackTraceElement : stackTraceElements) { - if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) - && stackTraceElement.getMethodName().equals("show")) { - return true; - } - } - return false; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java deleted file mode 100644 index 4d596351b3d0..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Handler; -import android.os.Looper; -import android.webkit.JavascriptInterface; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; - -/** - * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets - * up. - * - *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method - * channel to the Dart code. - */ -class JavaScriptChannel { - private final MethodChannel methodChannel; - private final String javaScriptChannelName; - private final Handler platformThreadHandler; - - /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through - */ - JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; - this.platformThreadHandler = platformThreadHandler; - } - - // Suppressing unused warning as this is invoked from JavaScript. - @SuppressWarnings("unused") - @JavascriptInterface - public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java deleted file mode 100644 index 1c865c9444e2..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java +++ /dev/null @@ -1,112 +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.webviewflutter; - -import android.os.Handler; -import android.os.IBinder; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; - -/** - * A fake View only exposed to InputMethodManager. - * - *

This follows a similar flow to Chromium's WebView (see - * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java). - * WebView itself bounces its InputConnection around several different threads. We follow its logic - * here to get the same working connection. - * - *

This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on - * the IME thread. The way that this is created in {@link - * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to - * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME - * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection. - */ -final class ThreadedInputConnectionProxyAdapterView extends View { - final Handler imeHandler; - final IBinder windowToken; - final View containerView; - final View rootView; - final View targetView; - - private boolean triggerDelayed = true; - private boolean isLocked = false; - private InputConnection cachedConnection; - - ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { - super(containerView.getContext()); - this.imeHandler = imeHandler; - this.containerView = containerView; - this.targetView = targetView; - windowToken = containerView.getWindowToken(); - rootView = containerView.getRootView(); - setFocusable(true); - setFocusableInTouchMode(true); - setVisibility(VISIBLE); - } - - /** Returns whether or not this is currently asynchronously acquiring an input connection. */ - boolean isTriggerDelayed() { - return triggerDelayed; - } - - /** Sets whether or not this should use its previously cached input connection. */ - void setLocked(boolean locked) { - isLocked = locked; - } - - /** - * This is expected to be called on the IME thread. See the setup required for this in {@link - * InputAwareWebView#checkInputConnectionProxy(View)}. - * - *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. - */ - @Override - public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { - triggerDelayed = false; - InputConnection inputConnection = - (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); - triggerDelayed = true; - cachedConnection = inputConnection; - return inputConnection; - } - - @Override - public boolean checkInputConnectionProxy(View view) { - return true; - } - - @Override - public boolean hasWindowFocus() { - // None of our views here correctly report they have window focus because of how we're embedding - // the platform view inside of a virtual display. - return true; - } - - @Override - public View getRootView() { - return rootView; - } - - @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override - public boolean isFocused() { - return true; - } - - @Override - public IBinder getWindowToken() { - return windowToken; - } - - @Override - public Handler getHandler() { - return imeHandler; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java deleted file mode 100644 index d3cd1d57cdae..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ /dev/null @@ -1,155 +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.webviewflutter; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** Builder used to create {@link android.webkit.WebView} objects. */ -public class WebViewBuilder { - - /** Factory used to create a new {@link android.webkit.WebView} instance. */ - static class WebViewFactory { - - /** - * Creates a new {@link android.webkit.WebView} instance. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is - * returned. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set - * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or - * IME, thread (see also {@link InputAwareWebView}) - * @return A new instance of the {@link android.webkit.WebView} object. - */ - static WebView create(Context context, boolean usesHybridComposition, View containerView) { - return usesHybridComposition - ? new WebView(context) - : new InputAwareWebView(context, containerView); - } - } - - private final Context context; - private final View containerView; - - private boolean enableDomStorage; - private boolean javaScriptCanOpenWindowsAutomatically; - private boolean supportMultipleWindows; - private boolean usesHybridComposition; - private WebChromeClient webChromeClient; - private DownloadListener downloadListener; - - /** - * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link - * WebViewFactory} object. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to - * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, - * thread (see also {@link InputAwareWebView}) - */ - WebViewBuilder(@NonNull final Context context, View containerView) { - this.context = context; - this.containerView = containerView; - } - - /** - * Sets whether the DOM storage API is enabled. The default value is {@code false}. - * - * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDomStorageEnabled(boolean flag) { - this.enableDomStorage = flag; - return this; - } - - /** - * Sets whether JavaScript is allowed to open windows automatically. This applies to the - * JavaScript function {@code window.open()}. The default value is {@code false}. - * - * @param flag {@code true} if JavaScript is allowed to open windows automatically. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { - this.javaScriptCanOpenWindowsAutomatically = flag; - return this; - } - - /** - * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link - * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is - * {@code false}. - * - * @param flag {@code true} if multiple windows are supported. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setSupportMultipleWindows(boolean flag) { - this.supportMultipleWindows = flag; - return this; - } - - /** - * Sets whether the hybrid composition should be used. - * - *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the - * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the - * {@link WebView} on Android versions below N. - * - * @param flag {@code true} if uses hybrid composition. The default is {@code false}. - * @return This builder. This value cannot be {@code null} - */ - public WebViewBuilder setUsesHybridComposition(boolean flag) { - this.usesHybridComposition = flag; - return this; - } - - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling - * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. - * - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { - this.webChromeClient = webChromeClient; - return this; - } - - /** - * 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. - * - * @param downloadListener an implementation of DownloadListener This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { - this.downloadListener = downloadListener; - return this; - } - - /** - * Build the {@link android.webkit.WebView} using the current settings. - * - * @return The {@link android.webkit.WebView} using the current settings. - */ - public WebView build() { - WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); - - WebSettings webSettings = webView.getSettings(); - webSettings.setDomStorageEnabled(enableDomStorage); - webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); - webSettings.setSupportMultipleWindows(supportMultipleWindows); - webView.setWebChromeClient(webChromeClient); - webView.setDownloadListener(downloadListener); - return webView; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java deleted file mode 100644 index 268d35a1e04c..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ /dev/null @@ -1,73 +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.webviewflutter; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; - -/** - * Java platform implementation of the webview_flutter plugin. - * - *

Register this in an add to app scenario to gracefully handle activity and context changes. - * - *

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} - * package instead. - */ -public class WebViewFlutterPlugin implements FlutterPlugin { - - private FlutterCookieManager flutterCookieManager; - - /** - * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to - * register it. - * - *

THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE - * PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least - * flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. Use the V1 embedding with {@link - * #registerWith(Registrar)} to use this plugin with older Flutter versions. - * - *

Registration should eventually be handled automatically by v2 of the - * GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694 - */ - public WebViewFlutterPlugin() {} - - /** - * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common} - * package. - * - *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link CameraPlugin}. - */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(registrar.messenger(), registrar.view())); - new FlutterCookieManager(registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java deleted file mode 100644 index 2c918584ba83..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java +++ /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. - -package io.flutter.plugins.webviewflutter; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.webkit.WebView; -import org.junit.Before; -import org.junit.Test; - -public class FlutterDownloadListenerTest { - private FlutterWebViewClient webViewClient; - private WebView webView; - - @Before - public void before() { - webViewClient = mock(FlutterWebViewClient.class); - webView = mock(WebView.class); - } - - @Test - public void onDownloadStart_should_notify_webViewClient() { - String url = "testurl.com"; - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); - } - - @Test - public void onDownloadStart_should_pass_webView() { - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.setWebView(webView); - downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(eq(webView), anyString()); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java deleted file mode 100644 index 86346ac08f16..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java +++ /dev/null @@ -1,60 +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.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.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import android.webkit.WebView; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -public class FlutterWebViewClientTest { - - MethodChannel mockMethodChannel; - WebView mockWebView; - - @Before - public void before() { - mockMethodChannel = mock(MethodChannel.class); - mockWebView = mock(WebView.class); - } - - @Test - public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(true); - - client.notifyDownload(mockWebView, url); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); - verify(mockMethodChannel) - .invokeMethod( - eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); - HashMap map = (HashMap) argumentCaptor.getValue(); - assertEquals(map.get("url"), url); - assertEquals(map.get("isForMainFrame"), true); - } - - @Test - public void - notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(false); - - client.notifyDownload(mockWebView, url); - verifyNoInteractions(mockMethodChannel); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java deleted file mode 100644 index 56d9db1ee493..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ /dev/null @@ -1,66 +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.webviewflutter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; - -public class FlutterWebViewTest { - private WebChromeClient mockWebChromeClient; - private DownloadListener mockDownloadListener; - private WebViewBuilder mockWebViewBuilder; - private WebView mockWebView; - - @Before - public void before() { - mockWebChromeClient = mock(WebChromeClient.class); - mockWebViewBuilder = mock(WebViewBuilder.class); - mockWebView = mock(WebView.class); - mockDownloadListener = mock(DownloadListener.class); - - when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) - .thenReturn(mockWebViewBuilder); - - when(mockWebViewBuilder.build()).thenReturn(mockWebView); - } - - @Test - public void createWebView_should_create_webview_with_default_configuration() { - FlutterWebView.createWebView( - mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); - - verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); - verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); - verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); - verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); - } - - private Map createParameterMap(boolean usesHybridComposition) { - Map params = new HashMap<>(); - params.put("usesHybridComposition", usesHybridComposition); - - return params; - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java deleted file mode 100644 index 423cb210c392..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; -import java.io.IOException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; -import org.mockito.MockedStatic.Verification; - -public class WebViewBuilderTest { - private Context mockContext; - private View mockContainerView; - private WebView mockWebView; - private MockedStatic mockedStaticWebViewFactory; - - @Before - public void before() { - mockContext = mock(Context.class); - mockContainerView = mock(View.class); - mockWebView = mock(WebView.class); - mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); - - mockedStaticWebViewFactory - .when( - new Verification() { - @Override - public void apply() { - WebViewFactory.create(mockContext, false, mockContainerView); - } - }) - .thenReturn(mockWebView); - } - - @After - public void after() { - mockedStaticWebViewFactory.close(); - } - - @Test - public void ctor_test() { - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - assertNotNull(builder); - } - - @Test - public void build_should_set_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - DownloadListener mockDownloadListener = mock(DownloadListener.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = - new WebViewBuilder(mockContext, mockContainerView) - .setDomStorageEnabled(true) - .setJavaScriptCanOpenWindowsAutomatically(true) - .setSupportMultipleWindows(true) - .setWebChromeClient(mockWebChromeClient) - .setDownloadListener(mockDownloadListener); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(true); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebSettings).setSupportMultipleWindows(true); - verify(mockWebView).setWebChromeClient(mockWebChromeClient); - verify(mockWebView).setDownloadListener(mockDownloadListener); - } - - @Test - public void build_should_use_default_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(false); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); - verify(mockWebSettings).setSupportMultipleWindows(false); - verify(mockWebView).setWebChromeClient(null); - verify(mockWebView).setDownloadListener(null); - } -} diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java deleted file mode 100644 index 131a5a3eb53a..000000000000 --- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ /dev/null @@ -1,49 +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.webviewflutter; - -import static org.junit.Assert.assertEquals; - -import android.webkit.WebViewClient; -import org.junit.Test; - -public class WebViewTest { - @Test - public void errorCodes() { - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), - "authentication"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), - "failedSslHandshake"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), - "proxyAuthentication"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), - "tooManyRequests"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), - "unsafeResource"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), - "unsupportedAuthScheme"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), - "unsupportedScheme"); - } -} 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 9a43699afb2b..8548d5b30ddd 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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.plugins.webviewflutterexample" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -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/assets/www/index.html b/packages/webview_flutter/webview_flutter/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

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

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file 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 0e128caa8f32..17548901bcb8 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 @@ -2,23 +2,50 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + 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:webview_flutter/platform_interface.dart'; -import 'package:webview_flutter/webview_flutter.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; -void main() { +Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + const bool _skipDueToIssue86757 = true; + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + 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(); @@ -27,7 +54,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -36,10 +63,10 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); - }, skip: true); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -48,7 +75,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -56,12 +83,34 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); + await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); - }, skip: true); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -72,7 +121,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -90,21 +139,20 @@ void main() { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', - headers: headers); + await controller.loadUrl(headersUrl, headers: headers); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + expect(currentUrl, headersUrl); await pageStarts.stream.firstWhere((String url) => url == currentUrl); await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + // 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(); @@ -144,100 +192,37 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } }, - javascriptMode: JavascriptMode.unrestricted, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -278,7 +263,7 @@ void main() { expect(customUserAgent2, 'Custom_User_Agent2'); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // 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 = @@ -290,7 +275,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: _globalKey, - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -328,7 +313,7 @@ void main() { final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, defaultPlatformUserAgent); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); group('Video playback policy', () { late String videoTestBase64; @@ -395,7 +380,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -424,7 +410,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -455,7 +441,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -483,16 +470,16 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); - Completer pageLoaded = Completer(); - Completer videoPlaying = Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); await tester.pumpWidget( Directionality( @@ -509,7 +496,7 @@ void main() { onMessageReceived: (JavascriptMessage message) { final double currentTime = double.parse(message.message); // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1) { + if (currentTime > 1 && !videoPlaying.isCompleted) { videoPlaying.complete(null); } }, @@ -523,7 +510,7 @@ void main() { ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -532,8 +519,8 @@ void main() { // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -541,10 +528,10 @@ void main() { testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); - Completer pageLoaded = Completer(); - Completer videoPlaying = Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); await tester.pumpWidget( Directionality( @@ -561,7 +548,7 @@ void main() { onMessageReceived: (JavascriptMessage message) { final double currentTime = double.parse(message.message); // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1) { + if (currentTime > 1 && !videoPlaying.isCompleted) { videoPlaying.complete(null); } }, @@ -575,7 +562,7 @@ void main() { ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -584,8 +571,8 @@ void main() { // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }, skip: Platform.isAndroid); }); @@ -651,7 +638,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -685,7 +673,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -721,7 +709,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -754,13 +743,13 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' + const String getTitleTest = ''' Some title @@ -780,6 +769,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -797,14 +787,20 @@ void main() { await pageStarted.future; await pageLoaded.future; + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + final String? title = await controller.getTitle(); expect(title, 'Some title'); }); group('Programmatic Scroll', () { - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -851,7 +847,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); int scrollPosX = await controller.getScrollX(); int scrollPosY = await controller.getScrollY(); @@ -877,198 +873,49 @@ void main() { scrollPosY = await controller.getScrollY(); expect(scrollPosX, X_SCROLL * 2); expect(scrollPosY, Y_SCROLL * 2); - }, skip: Platform.isAndroid); + }, skip: Platform.isAndroid && _skipDueToIssue86757); }); - group('SurfaceAndroidWebView', () { + // Minimial end-to-end testing of the legacy Android implementation. + group('AndroidWebView (virtual display)', () { setUpAll(() { - WebView.platform = SurfaceAndroidWebView(); + WebView.platform = AndroidWebView(); }); tearDownAll(() { WebView.platform = null; }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' - - - - - - -
- - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); + testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); - + final Completer loadCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + key: GlobalKey(), + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, onPageFinished: (String url) { - pageLoaded.complete(null); + loadCompleter.complete(); }, ), ), ); - - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - - await tester.pumpAndSettle(Duration(seconds: 3)); - - // Check scrollTo() - const int X_SCROLL = 123; - const int Y_SCROLL = 321; - - await controller.scrollTo(X_SCROLL, Y_SCROLL); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); - expect(X_SCROLL, scrollPosX); - expect(Y_SCROLL, scrollPosY); - - // Check scrollBy() (on top of scrollTo()) - await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(X_SCROLL * 2, scrollPosX); - expect(Y_SCROLL * 2, scrollPosY); - }, skip: true); - - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('inputs are scrolled into view when focused', - (WidgetTester tester) async { - final String scrollTestPage = ''' - - - - - - -
- - - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.runAsync(() async { - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 200, - height: 200, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ), - ); - await Future.delayed(Duration(milliseconds: 20)); - await tester.pump(); - }); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - final String viewportRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(viewport.getBoundingClientRect())'); - final Map viewportRectRelativeToViewport = - jsonDecode(viewportRectJSON); - - // Check that the input is originally outside of the viewport. - - final String initialInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map initialInputClientRectRelativeToViewport = - jsonDecode(initialInputClientRectJSON); - - expect( - initialInputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isFalse); - - await controller.evaluateJavascript('inputEl.focus()'); - - // Check that focusing the input brought it into view. - - final String lastInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map lastInputClientRectRelativeToViewport = - jsonDecode(lastInputClientRectJSON); - - expect( - lastInputClientRectRelativeToViewport['top'] >= - viewportRectRelativeToViewport['top'], - isTrue); - expect( - lastInputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isTrue); - - expect( - lastInputClientRectRelativeToViewport['left'] >= - viewportRectRelativeToViewport['left'], - isTrue); - expect( - lastInputClientRectRelativeToViewport['right'] <= - viewportRectRelativeToViewport['right'], - isTrue); - }, skip: true); - }); + await loadCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + }, skip: !Platform.isAndroid || _skipDueToIssue86757); group('NavigationDelegate', () { - final String blankPage = ""; - final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + - base64Encode(const Utf8Encoder().convert(blankPage)); + const String 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 = @@ -1097,12 +944,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1163,7 +1009,7 @@ void main() { testWidgets( 'onWebResourceError only called for main frame', (WidgetTester tester) async { - final String iframeTest = ''' + const String iframeTest = ''' @@ -1229,7 +1075,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order @@ -1269,12 +1115,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }); }); @@ -1290,7 +1135,7 @@ void main() { height: 300, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1301,9 +1146,10 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + 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 = @@ -1325,16 +1171,13 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); - }, - // Flaky on Android: https://github.com/flutter/flutter/issues/86757 - skip: Platform.isAndroid); + expect(currentUrl, primaryUrl); + }, skip: Platform.isAndroid && _skipDueToIssue86757); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets( 'can open new window and go back', (WidgetTester tester) async { @@ -1353,95 +1196,72 @@ void main() { onPageFinished: (String url) { pageLoaded.complete(); }, - initialUrl: 'https://flutter.dev', + initialUrl: primaryUrl, ), ), ); final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller - .evaluateJavascript('window.open("https://www.google.com/")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.currentUrl(), completion(secondaryUrl)); expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); }, - skip: true, + skip: _skipDueToIssue86757, ); testWidgets( - 'javascript does not run in parent window', + 'clearCache should clear local storage', (WidgetTester tester) async { - final String iframe = ''' - - - '''; - final String iframeTestBase64 = - base64Encode(const Utf8Encoder().convert(iframe)); - - final String openWindowTest = ''' - - - - XSS test - - - - - - '''; - final String openWindowTestBase64 = - base64Encode(const Utf8Encoder().convert(openWindowTest)); final Completer controllerCompleter = Completer(); - final Completer pageLoadCompleter = Completer(); - + final Completer onPageFinished = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => onPageFinished.complete(), onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, - javascriptMode: JavascriptMode.unrestricted, - initialUrl: - 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', - onPageFinished: (String url) { - pageLoadCompleter.complete(); - }, ), ), ); final WebViewController controller = await controllerCompleter.future; - await pageLoadCompleter.future; + await onPageFinished.future; + + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); - expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); expect( - controller.evaluateJavascript( - 'document.querySelector("p") && document.querySelector("p").textContent'), - completion('null'), + controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ), + completion(_webviewString('Tom')), + ); + + await controller.clearCache(); + + expect( + controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ), + completion(_webviewNull()), ); }, - skip: !Platform.isAndroid, + // TODO(bparrishMines): Unskip once https://github.com/flutter/plugins/pull/5086 lands and is published. + skip: Platform.isAndroid, ); } @@ -1454,15 +1274,107 @@ 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 _evaluateJavascript(controller, 'navigator.userAgent;'); + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavascriptReturningResult( WebViewController controller, String js) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript(js); + return await controller.runJavascriptReturningResult(js); + } + return jsonDecode(await controller.runJavascriptReturningResult(js)) + as String; +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); } - return jsonDecode(await controller.evaluateJavascript(js)); } diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 62428d041adf..0759b31a2f25 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,13 +11,13 @@ 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 63D2F2FB307F1F037702C198 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BEC8CD326B252E47ABE6C037 /* 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 */; }; - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + E6159E2B6496F35B1D4F4096 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0ABA59F25635F077C9EA161 /* libPods-Runner.a */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -52,11 +52,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 127772EEA7782174BE0D74B5 /* 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 = ""; }; + 11DF059E983DF25F078B44CC /* 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 = ""; }; 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 = ""; }; - 27CC950C9005575711528C12 /* 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 = ""; }; + 3CEFE8F0E91B9792E4EE427B /* 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 = ""; }; + 4D2B3F45D8E6CA81EA52591E /* 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 = ""; }; + 5D19D984A61169BB95DB0FED /* 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 = ""; }; 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -64,7 +66,6 @@ 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 = ""; }; - 8B845D8FBDE0AAD6BE1A0386 /* 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; }; @@ -73,9 +74,8 @@ 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 = ""; }; - C475C484BD510DD9CB2E403C /* 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 = ""; }; - E14113434CCE6D3186B5CBC3 /* 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 = ""; }; - F674B2A05DAC369B4FF27850 /* 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 = ""; }; + BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C0ABA59F25635F077C9EA161 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -86,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + 63D2F2FB307F1F037702C198 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + E6159E2B6496F35B1D4F4096 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,6 +108,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 00D2395F7DDFEE571DF3C0B1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0ABA59F25635F077C9EA161 /* libPods-Runner.a */, + BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,8 +146,8 @@ 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, F7151F75266057800028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, + EA36D6F90B795550E32A139A /* Pods */, + 00D2395F7DDFEE571DF3C0B1 /* Frameworks */, ); sourceTree = ""; }; @@ -176,24 +185,16 @@ name = "Supporting Files"; sourceTree = ""; }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { - isa = PBXGroup; - children = ( - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + EA36D6F90B795550E32A139A /* Pods */ = { isa = PBXGroup; children = ( - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + 4D2B3F45D8E6CA81EA52591E /* Pods-Runner.debug.xcconfig */, + 11DF059E983DF25F078B44CC /* Pods-Runner.release.xcconfig */, + 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */, + 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; F7151F75266057800028CB91 /* RunnerUITests */ = { @@ -212,7 +213,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + EA0C9BB56C9A98B4F095051B /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -231,7 +232,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 1B3EA6BF26F6D525A8503093 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -338,41 +339,41 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 1B3EA6BF26F6D525A8503093 /* [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 = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); 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"; + 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; }; - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Thin Binary"; 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; + 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"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -388,18 +389,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + EA0C9BB56C9A98B4F095051B /* [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", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -473,7 +478,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -487,7 +492,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m index d193be745972..d6870dc9a29c 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m +++ b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -6,7 +6,7 @@ @import os.log; @interface FLTWebViewUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @implementation FLTWebViewUITests @@ -19,22 +19,22 @@ - (void)setUp { } - (void)testUserAgent { - XCUIApplication* app = self.app; - XCUIElement* menu = app.buttons[@"Show menu"]; + XCUIApplication *app = self.app; + XCUIElement *menu = app.buttons[@"Show menu"]; if (![menu waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find menu"); } [menu tap]; - XCUIElement* userAgent = app.buttons[@"Show user agent"]; + XCUIElement *userAgent = app.buttons[@"Show user agent"]; if (![userAgent waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Show user agent"); } - NSPredicate* userAgentPredicate = + NSPredicate *userAgentPredicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; - XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCUIElement *userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; XCTAssertFalse(userAgentPopUp.exists); [userAgent tap]; if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { @@ -44,15 +44,15 @@ - (void)testUserAgent { } - (void)testCache { - XCUIApplication* app = self.app; - XCUIElement* menu = app.buttons[@"Show menu"]; + XCUIApplication *app = self.app; + XCUIElement *menu = app.buttons[@"Show menu"]; if (![menu waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find menu"); } [menu tap]; - XCUIElement* clearCache = app.buttons[@"Clear cache"]; + XCUIElement *clearCache = app.buttons[@"Clear cache"]; if (![clearCache waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Clear cache"); @@ -61,21 +61,21 @@ - (void)testCache { [menu tap]; - XCUIElement* listCache = app.buttons[@"List cache"]; + XCUIElement *listCache = app.buttons[@"List cache"]; if (![listCache waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find List cache"); } [listCache tap]; - XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + XCUIElement *emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find empty cache pop up"); } [menu tap]; - XCUIElement* addCache = app.buttons[@"Add to cache"]; + XCUIElement *addCache = app.buttons[@"Add to cache"]; if (![addCache waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Add to cache"); @@ -89,7 +89,7 @@ - (void)testCache { } [listCache tap]; - XCUIElement* cachePopup = + XCUIElement *cachePopup = app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" @"localStorage\":\"dummy_entry\"}}"]; if (![cachePopup waitForExistenceWithTimeout:30.0]) { diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 88256cc66287..79197b02315c 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -7,10 +7,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; -void main() => runApp(MaterialApp(home: WebViewExample())); +void main() => runApp(const MaterialApp(home: WebViewExample())); const String kNavigationExamplePage = ''' @@ -27,9 +30,53 @@ The navigation delegate is set to block navigation to the youtube website. '''; +const String kLocalExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

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

+ + + +'''; + +const String kTransparentBackgroundPage = ''' + + + + Transparent background test + + + +
+

Transparent background test

+
+
+ + +'''; + 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 { @@ -39,52 +86,52 @@ class _WebViewExampleState extends State { @override void initState() { super.initState(); - if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + if (Platform.isAndroid) { + WebView.platform = SurfaceAndroidWebView(); + } } @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.green, appBar: AppBar( title: const Text('Flutter WebView example'), // 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://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, - ); - }), + body: WebView( + 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), + ), floatingActionButton: favoriteButton(), ); } @@ -93,8 +140,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)), ); }); @@ -105,19 +151,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), + ); }); } } @@ -130,13 +181,21 @@ enum MenuOptions { listCache, clearCache, navigationDelegate, + doPostRequest, + loadLocalFile, + loadFlutterAsset, + loadHtmlString, + transparentBackground, + setCookie, } 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) { @@ -145,6 +204,7 @@ class SampleMenu extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot controller) { return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), onSelected: (MenuOptions value) { switch (value) { case MenuOptions.showUserAgent: @@ -168,13 +228,31 @@ class SampleMenu extends StatelessWidget { case MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; + case MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, 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, @@ -200,26 +278,50 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), ], ); }, ); } - void _onShowUserAgent( + Future _onShowUserAgent( WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); } - void _onListCookies( + Future _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + await controller.runJavascriptReturningResult('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -231,48 +333,91 @@ class SampleMenu extends StatelessWidget { )); } - void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + Future _onAddToCache( + 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.'), )); } - void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + 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))'); } - void _onClearCache(WebViewController controller, BuildContext context) async { + Future _onClearCache( + WebViewController controller, BuildContext context) async { await controller.clearCache(); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( - content: Text("Cache cleared."), + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), )); } - void _onClearCookies(BuildContext context) async { + Future _onClearCookies(BuildContext context) async { final bool hadCookies = await cookieManager.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; } - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } - void _onNavigationDelegateExample( + Future _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await cookieManager.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + + Future _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + + Future _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalExamplePage); + } + + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -286,11 +431,23 @@ class SampleMenu extends StatelessWidget { children: cookieWidgets.toList(), ); } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File( + {tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); + + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); + + return indexFile.path; + } } 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; @@ -302,7 +459,7 @@ class NavigationControls extends StatelessWidget { (BuildContext context, AsyncSnapshot snapshot) { final bool webViewReady = snapshot.connectionState == ConnectionState.done; - final WebViewController controller = snapshot.data!; + final WebViewController? controller = snapshot.data; return Row( children: [ IconButton( @@ -310,12 +467,11 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { + if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( - const SnackBar(content: Text("No back history item")), + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), ); return; } @@ -326,13 +482,12 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { + 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")), + content: Text('No forward history item')), ); return; } @@ -343,7 +498,7 @@ class NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller!.reload(); }, ), ], diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 6b668eb96af3..f1da7cd17b7e 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -4,11 +4,12 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter + path_provider: ^2.0.6 webview_flutter: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -19,10 +20,10 @@ dependencies: dev_dependencies: espresso: ^0.1.0+2 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter pedantic: ^1.10.0 @@ -32,3 +33,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css 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/ios/Assets/.gitkeep b/packages/webview_flutter/webview_flutter/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h deleted file mode 100644 index 8fe331875250..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.h +++ /dev/null @@ -1,14 +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 - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m deleted file mode 100644 index eb7c856b250d..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTCookieManager.m +++ /dev/null @@ -1,44 +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 "FLTCookieManager.h" - -@implementation FLTCookieManager { -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTCookieManager *instance = [[FLTCookieManager alloc] init]; - - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"clearCookies"]) { - [self clearCookies:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)clearCookies:(FlutterResult)result { - NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; - WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; - - void (^deleteAndNotify)(NSArray *) = - ^(NSArray *cookies) { - BOOL hasCookies = cookies.count > 0; - [dataStore removeDataOfTypes:websiteDataTypes - forDataRecords:cookies - completionHandler:^{ - result(@(hasCookies)); - }]; - }; - - [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h deleted file mode 100644 index 31edadc8cc05..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h +++ /dev/null @@ -1,21 +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; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m deleted file mode 100644 index 8b7ee7d0cfb7..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m +++ /dev/null @@ -1,116 +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 { - [_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/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h deleted file mode 100644 index 96af4ef6c578..000000000000 --- a/packages/webview_flutter/webview_flutter/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/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m deleted file mode 100644 index 8e7af4649aa0..000000000000 --- a/packages/webview_flutter/webview_flutter/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/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h deleted file mode 100644 index 2a80c7d886f2..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface FLTWebViewFlutterPlugin : NSObject -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m deleted file mode 100644 index 9f01416acc6a..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.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 "FLTWebViewFlutterPlugin.h" -#import "FLTCookieManager.h" -#import "FlutterWebView.h" - -@implementation FLTWebViewFlutterPlugin - -+ (void)registerWithRegistrar:(NSObject*)registrar { - FLTWebViewFactory* webviewFactory = - [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; - [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; - [FLTCookieManager registerWithRegistrar:registrar]; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h deleted file mode 100644 index 6e795f7d1528..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWebViewController : NSObject - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger; - -- (UIView*)view; -@end - -@interface FLTWebViewFactory : NSObject -- (instancetype)initWithMessenger:(NSObject*)messenger; -@end - -/** - * The WkWebView used for the plugin. - * - * This class overrides some methods in `WKWebView` to serve the needs for the plugin. - */ -@interface FLTWKWebView : WKWebView -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m deleted file mode 100644 index 1604f2756f31..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/Classes/FlutterWebView.m +++ /dev/null @@ -1,475 +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 "FlutterWebView.h" -#import "FLTWKNavigationDelegate.h" -#import "FLTWKProgressionDelegate.h" -#import "JavaScriptChannelHandler.h" - -@implementation FLTWebViewFactory { - NSObject* _messenger; -} - -- (instancetype)initWithMessenger:(NSObject*)messenger { - self = [super init]; - if (self) { - _messenger = messenger; - } - return self; -} - -- (NSObject*)createArgsCodec { - return [FlutterStandardMessageCodec sharedInstance]; -} - -- (NSObject*)createWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args { - FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame - viewIdentifier:viewId - arguments:args - binaryMessenger:_messenger]; - return webviewController; -} - -@end - -@implementation FLTWKWebView - -- (void)setFrame:(CGRect)frame { - [super setFrame:frame]; - self.scrollView.contentInset = UIEdgeInsetsZero; - // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of - // webview's contentInsets. - // 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); - } -} - -@end - -@implementation FLTWebViewController { - FLTWKWebView* _webView; - int64_t _viewId; - FlutterMethodChannel* _channel; - NSString* _currentUrl; - // The set of registered JavaScript channel names. - NSMutableSet* _javaScriptChannelNames; - FLTWKNavigationDelegate* _navigationDelegate; - FLTWKProgressionDelegate* _progressionDelegate; -} - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger { - if (self = [super init]) { - _viewId = viewId; - - NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId]; - _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; - _javaScriptChannelNames = [[NSMutableSet alloc] init]; - - WKUserContentController* userContentController = [[WKUserContentController alloc] init]; - if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { - NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"]; - [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; - [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; - } - - NSDictionary* settings = args[@"settings"]; - - WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; - [self applyConfigurationSettings:settings toConfiguration:configuration]; - configuration.userContentController = userContentController; - [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] - inConfiguration:configuration]; - - _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; - _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; - _webView.UIDelegate = self; - _webView.navigationDelegate = _navigationDelegate; - __weak __typeof__(self) weakSelf = self; - [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { - [weakSelf onMethodCall:call result:result]; - }]; - - if (@available(iOS 11.0, *)) { - _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - if (@available(iOS 13.0, *)) { - _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; - } - } - - [self applySettings:settings]; - // TODO(amirh): return an error if apply settings failed once it's possible to do so. - // https://github.com/flutter/flutter/issues/36228 - - NSString* initialUrl = args[@"initialUrl"]; - if ([initialUrl isKindOfClass:[NSString class]]) { - [self loadUrl:initialUrl]; - } - } - return self; -} - -- (void)dealloc { - if (_progressionDelegate != nil) { - [_progressionDelegate stopObservingProgress:_webView]; - } -} - -- (UIView*)view { - return _webView; -} - -- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"updateSettings"]) { - [self onUpdateSettings:call result:result]; - } else if ([[call method] isEqualToString:@"loadUrl"]) { - [self onLoadUrl:call result:result]; - } else if ([[call method] isEqualToString:@"canGoBack"]) { - [self onCanGoBack:call result:result]; - } else if ([[call method] isEqualToString:@"canGoForward"]) { - [self onCanGoForward:call result:result]; - } else if ([[call method] isEqualToString:@"goBack"]) { - [self onGoBack:call result:result]; - } else if ([[call method] isEqualToString:@"goForward"]) { - [self onGoForward:call result:result]; - } else if ([[call method] isEqualToString:@"reload"]) { - [self onReload:call result:result]; - } else if ([[call method] isEqualToString:@"currentUrl"]) { - [self onCurrentUrl:call result:result]; - } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { - [self onEvaluateJavaScript:call result:result]; - } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { - [self onAddJavaScriptChannels:call result:result]; - } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { - [self onRemoveJavaScriptChannels:call result:result]; - } else if ([[call method] isEqualToString:@"clearCache"]) { - [self clearCache:result]; - } else if ([[call method] isEqualToString:@"getTitle"]) { - [self onGetTitle:result]; - } else if ([[call method] isEqualToString:@"scrollTo"]) { - [self onScrollTo:call result:result]; - } else if ([[call method] isEqualToString:@"scrollBy"]) { - [self onScrollBy:call result:result]; - } else if ([[call method] isEqualToString:@"getScrollX"]) { - [self getScrollX:call result:result]; - } else if ([[call method] isEqualToString:@"getScrollY"]) { - [self getScrollY:call result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* error = [self applySettings:[call arguments]]; - if (error == nil) { - result(nil); - return; - } - result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); -} - -- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - if (![self loadRequest:[call arguments]]) { - result([FlutterError - errorWithCode:@"loadUrl_failed" - message:@"Failed parsing the URL" - details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); - } else { - result(nil); - } -} - -- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { - BOOL canGoBack = [_webView canGoBack]; - result(@(canGoBack)); -} - -- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { - BOOL canGoForward = [_webView canGoForward]; - result(@(canGoForward)); -} - -- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView goBack]; - result(nil); -} - -- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView goForward]; - result(nil); -} - -- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView reload]; - result(nil); -} - -- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - _currentUrl = [[_webView URL] absoluteString]; - result(_currentUrl); -} - -- (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* jsString = [call arguments]; - if (!jsString) { - result([FlutterError errorWithCode:@"evaluateJavaScript_failed" - message:@"JavaScript String cannot be null" - details:nil]); - return; - } - [_webView evaluateJavaScript:jsString - completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { - if (error) { - result([FlutterError - errorWithCode:@"evaluateJavaScript_failed" - message:@"Failed evaluating JavaScript" - details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@", - jsString, error]]); - } else { - result([NSString stringWithFormat:@"%@", evaluateResult]); - } - }]; -} - -- (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { - NSArray* channelNames = [call arguments]; - NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; - [_javaScriptChannelNames addObjectsFromArray:channelNames]; - [self registerJavaScriptChannels:channelNamesSet - controller:_webView.configuration.userContentController]; - result(nil); -} - -- (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { - // WkWebView does not support removing a single user script, so instead we remove all - // user scripts, all message handlers. And re-register channels that shouldn't be removed. - [_webView.configuration.userContentController removeAllUserScripts]; - for (NSString* channelName in _javaScriptChannelNames) { - [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName]; - } - - NSArray* channelNamesToRemove = [call arguments]; - for (NSString* channelName in channelNamesToRemove) { - [_javaScriptChannelNames removeObject:channelName]; - } - - [self registerJavaScriptChannels:_javaScriptChannelNames - controller:_webView.configuration.userContentController]; - result(nil); -} - -- (void)clearCache:(FlutterResult)result { - NSSet* cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; - WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; - NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; - [dataStore removeDataOfTypes:cacheDataTypes - modifiedSince:dateFrom - completionHandler:^{ - result(nil); - }]; -} - -- (void)onGetTitle:(FlutterResult)result { - NSString* title = _webView.title; - result(title); -} - -- (void)onScrollTo:(FlutterMethodCall*)call result:(FlutterResult)result { - NSDictionary* arguments = [call arguments]; - int x = [arguments[@"x"] intValue]; - int y = [arguments[@"y"] intValue]; - - _webView.scrollView.contentOffset = CGPointMake(x, y); - result(nil); -} - -- (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result { - CGPoint contentOffset = _webView.scrollView.contentOffset; - - NSDictionary* arguments = [call arguments]; - int x = [arguments[@"x"] intValue] + contentOffset.x; - int y = [arguments[@"y"] intValue] + contentOffset.y; - - _webView.scrollView.contentOffset = CGPointMake(x, y); - result(nil); -} - -- (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result { - int offsetX = _webView.scrollView.contentOffset.x; - result(@(offsetX)); -} - -- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { - int offsetY = _webView.scrollView.contentOffset.y; - result(@(offsetY)); -} - -// Returns nil when successful, or an error message when one or more keys are unknown. -- (NSString*)applySettings:(NSDictionary*)settings { - NSMutableArray* unknownKeys = [[NSMutableArray alloc] init]; - for (NSString* key in settings) { - if ([key isEqualToString:@"jsMode"]) { - NSNumber* mode = settings[key]; - [self updateJsMode:mode]; - } else if ([key isEqualToString:@"hasNavigationDelegate"]) { - NSNumber* hasDartNavigationDelegate = settings[key]; - _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; - } else if ([key isEqualToString:@"hasProgressTracking"]) { - NSNumber* hasProgressTrackingValue = settings[key]; - bool hasProgressTracking = [hasProgressTrackingValue boolValue]; - if (hasProgressTracking) { - _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView - channel:_channel]; - } - } else if ([key isEqualToString:@"debuggingEnabled"]) { - // no-op debugging is always enabled on iOS. - } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { - NSNumber* allowsBackForwardNavigationGestures = settings[key]; - _webView.allowsBackForwardNavigationGestures = - [allowsBackForwardNavigationGestures boolValue]; - } else if ([key isEqualToString:@"userAgent"]) { - NSString* userAgent = settings[key]; - [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; - } else { - [unknownKeys addObject:key]; - } - } - if ([unknownKeys count] == 0) { - return nil; - } - return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", - [unknownKeys componentsJoinedByString:@", "]]; -} - -- (void)applyConfigurationSettings:(NSDictionary*)settings - toConfiguration:(WKWebViewConfiguration*)configuration { - NSAssert(configuration != _webView.configuration, - @"configuration needs to be updated before webView.configuration."); - for (NSString* key in settings) { - if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { - NSNumber* allowsInlineMediaPlayback = settings[key]; - configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; - } - } -} - -- (void)updateJsMode:(NSNumber*)mode { - WKPreferences* preferences = [[_webView configuration] preferences]; - switch ([mode integerValue]) { - case 0: // disabled - [preferences setJavaScriptEnabled:NO]; - break; - case 1: // unrestricted - [preferences setJavaScriptEnabled:YES]; - break; - default: - NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode); - } -} - -- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy - inConfiguration:(WKWebViewConfiguration*)configuration { - switch ([policy integerValue]) { - case 0: // require_user_action_for_all_media_types - if (@available(iOS 10.0, *)) { - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; - } else { - configuration.requiresUserActionForMediaPlayback = true; - } - break; - case 1: // always_allow - if (@available(iOS 10.0, *)) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; -#pragma clang diagnostic pop - } else { - configuration.requiresUserActionForMediaPlayback = false; - } - break; - default: - NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); - } -} - -- (bool)loadRequest:(NSDictionary*)request { - if (!request) { - return false; - } - - NSString* url = request[@"url"]; - if ([url isKindOfClass:[NSString class]]) { - id headers = request[@"headers"]; - if ([headers isKindOfClass:[NSDictionary class]]) { - return [self loadUrl:url withHeaders:headers]; - } else { - return [self loadUrl:url]; - } - } - - return false; -} - -- (bool)loadUrl:(NSString*)url { - return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; -} - -- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { - NSURL* nsUrl = [NSURL URLWithString:url]; - if (!nsUrl) { - return false; - } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; - [request setAllHTTPHeaderFields:headers]; - [_webView loadRequest:request]; - return true; -} - -- (void)registerJavaScriptChannels:(NSSet*)channelNames - controller:(WKUserContentController*)userContentController { - for (NSString* channelName in channelNames) { - FLTJavaScriptChannel* channel = - [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel - javaScriptChannelName:channelName]; - [userContentController addScriptMessageHandler:channel name:channelName]; - NSString* wrapperSource = [NSString - stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName]; - WKUserScript* wrapperScript = - [[WKUserScript alloc] initWithSource:wrapperSource - injectionTime:WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly:NO]; - [userContentController addUserScript:wrapperScript]; - } -} - -- (void)updateUserAgent:(NSString*)userAgent { - [_webView setCustomUserAgent:userAgent]; -} - -#pragma mark WKUIDelegate - -- (WKWebView*)webView:(WKWebView*)webView - createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration - forNavigationAction:(WKNavigationAction*)navigationAction - windowFeatures:(WKWindowFeatures*)windowFeatures { - if (!navigationAction.targetFrame.isMainFrame) { - [webView loadRequest:navigationAction.request]; - } - - return nil; -} - -@end diff --git a/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.h deleted file mode 100644 index a0a5ec657295..000000000000 --- a/packages/webview_flutter/webview_flutter/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/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter/ios/Classes/JavaScriptChannelHandler.m deleted file mode 100644 index ec9a363a4b2e..000000000000 --- a/packages/webview_flutter/webview_flutter/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/ios/webview_flutter.podspec b/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec deleted file mode 100644 index 2e021994b8f4..000000000000 --- a/packages/webview_flutter/webview_flutter/ios/webview_flutter.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'webview_flutter' - s.version = '0.0.1' - s.summary = 'A WebView Plugin for Flutter.' - s.description = <<-DESC -A Flutter plugin that provides a WebView widget. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter' } - s.documentation_url = 'https://pub.dev/packages/webview_flutter' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } -end diff --git a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart index 92aa87b7480f..48f74346fe61 100644 --- a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart @@ -2,547 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -import 'webview_flutter.dart'; - -/// Interface for callbacks made by [WebViewPlatformController]. -/// -/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. -/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. -abstract class WebViewPlatformCallbacksHandler { - /// Invoked by [WebViewPlatformController] when a JavaScript channel message is received. - void onJavaScriptChannelMessage(String channel, String message); - - /// Invoked by [WebViewPlatformController] when a navigation request is pending. - /// - /// If true is returned the navigation is allowed, otherwise it is blocked. - FutureOr onNavigationRequest( - {required String url, required bool isForMainFrame}); - - /// Invoked by [WebViewPlatformController] when a page has started loading. - void onPageStarted(String url); - - /// Invoked by [WebViewPlatformController] when a page has finished loading. - void onPageFinished(String url); - - /// Invoked by [WebViewPlatformController] when a page is loading. - /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. - void onProgress(int progress); - - /// Report web resource loading error to the host application. - void onWebResourceError(WebResourceError error); -} - -/// 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. -class WebResourceError { - /// Creates a new [WebResourceError] - /// - /// A user should not need to instantiate this class, but will receive one in - /// [WebResourceErrorCallback]. - WebResourceError({ - required this.errorCode, - required this.description, - this.domain, - this.errorType, - this.failingUrl, - }) : assert(errorCode != null), - assert(description != null); - - /// Raw code of the error from the respective platform. - /// - /// On Android, the error code will be a constant from a - /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and - /// will have a corresponding [errorType]. - /// - /// On iOS, the error code will be a constant from `NSError.code` in - /// Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. Some possible error codes - /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. - final int errorCode; - - /// The domain of where to find the error code. - /// - /// This field is only available on iOS and represents a "domain" from where - /// the [errorCode] is from. This value is taken directly from an `NSError` - /// in Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. - final String? domain; - - /// 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. - /// - /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType? errorType; - - /// Gets the URL for which the resource request was made. - /// - /// This value is not provided on iOS. Alternatively, you can keep track of - /// the last values provided to [WebViewPlatformController.loadUrl]. - final String? failingUrl; -} - -/// Interface for talking to the webview's platform implementation. -/// -/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is -/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. -/// -/// Platform implementations that live in a separate package 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 [WebViewPlatformController] methods. -abstract class WebViewPlatformController { - /// Creates a new WebViewPlatform. - /// - /// Callbacks made by the WebView will be delegated to `handler`. - /// - /// The `handler` parameter must not be null. - WebViewPlatformController(WebViewPlatformCallbacksHandler handler); - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, - Map? headers, - ) { - throw UnimplementedError( - "WebView loadUrl is not implemented on the current platform"); - } - - /// Updates the webview settings. - /// - /// Any non null field in `settings` will be set as the new setting value. - /// All null fields in `settings` are ignored. - Future updateSettings(WebSettings setting) { - throw UnimplementedError( - "WebView updateSettings 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( - "WebView currentUrl is not implemented on the current platform"); - } - - /// Checks whether there's a back history item. - Future canGoBack() { - throw UnimplementedError( - "WebView canGoBack is not implemented on the current platform"); - } - - /// Checks whether there's a forward history item. - Future canGoForward() { - throw UnimplementedError( - "WebView 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( - "WebView 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( - "WebView goForward is not implemented on the current platform"); - } - - /// Reloads the current URL. - Future reload() { - throw UnimplementedError( - "WebView 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. - /// 4. Local Storage. - Future clearCache() { - throw UnimplementedError( - "WebView clearCache is not implemented on the current platform"); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { - throw UnimplementedError( - "WebView evaluateJavascript is not implemented on the current platform"); - } - - /// Adds new JavaScript channels to the set of enabled channels. - /// - /// For each value in this list the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - /// - /// See also: [CreationParams.javascriptChannelNames]. - Future addJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView addJavascriptChannels is not implemented on the current platform"); - } - - /// Removes JavaScript channel names from the set of enabled channels. - /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through - /// [CreationParams.javascriptChannelNames]. - Future removeJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView removeJavascriptChannels is not implemented on the current platform"); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - throw UnimplementedError( - "WebView 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( - "WebView 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( - "WebView scrollBy is not implemented on the current platform"); - } - - /// Return the horizontal scroll position of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - throw UnimplementedError( - "WebView getScrollX is not implemented on the current platform"); - } - - /// Return the vertical scroll position of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - throw UnimplementedError( - "WebView getScrollY is not implemented on the current platform"); - } -} - -/// A single setting for configuring a WebViewPlatform which may be absent. -class WebSetting { - /// Constructs an absent setting instance. - /// - /// The [isPresent] field for the instance will be false. - /// - /// Accessing [value] for an absent instance will throw. - WebSetting.absent() - : _value = null, - isPresent = false; - - /// Constructs a setting of the given `value`. - /// - /// The [isPresent] field for the instance will be true. - WebSetting.of(T value) - : _value = value, - isPresent = true; - - final T? _value; - - /// The setting's value. - /// - /// Throws if [WebSetting.isPresent] is false. - T get value { - if (!isPresent) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - assert(isPresent); - // The intention of this getter is to return T whether it is nullable or - // not whereas _value is of type T? since _value can be null even when - // T is not nullable (when isPresent == false). - // - // We promote _value to T using `as T` instead of `!` operator to handle - // the case when _value is legitimately null (and T is a nullable type). - // `!` operator would always throw if _value is null. - return _value as T; - } - - /// True when this web setting instance contains a value. - /// - /// When false the [WebSetting.value] getter throws. - final bool isPresent; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - final WebSetting typedOther = other as WebSetting; - return typedOther.isPresent == isPresent && typedOther._value == _value; - } - - @override - int get hashCode => hashValues(_value, isPresent); -} - -/// Settings for configuring a WebViewPlatform. -/// -/// Initial settings are passed as part of [CreationParams], settings updates are sent with -/// [WebViewPlatform#updateSettings]. -/// -/// The `userAgent` parameter must not be null. -class WebSettings { - /// Construct an instance with initial settings. Future setting changes can be - /// sent with [WebviewPlatform#updateSettings]. - /// - /// The `userAgent` parameter must not be null. - WebSettings({ - this.javascriptMode, - this.hasNavigationDelegate, - this.hasProgressTracking, - this.debuggingEnabled, - this.gestureNavigationEnabled, - this.allowsInlineMediaPlayback, - required this.userAgent, - }) : assert(userAgent != null); - - /// The JavaScript execution mode to be used by the webview. - final JavascriptMode? javascriptMode; - - /// Whether the [WebView] has a [NavigationDelegate] set. - final bool? hasNavigationDelegate; - - /// Whether the [WebView] should track page loading progress. - /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress. - final bool? hasProgressTracking; - - /// Whether to enable the platform's webview content debugging tools. - /// - /// See also: [WebView.debuggingEnabled]. - final bool? debuggingEnabled; - - /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS. - /// - /// This will have no effect on Android. - final bool? allowsInlineMediaPlayback; - - /// The value used for the HTTP `User-Agent:` request header. - /// - /// If [userAgent.value] is null the platform's default user agent should be used. - /// - /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the - /// last time it was set. - /// - /// See also [WebView.userAgent]. - final WebSetting userAgent; - - /// Whether to allow swipe based navigation in iOS. - /// - /// See also: [WebView.gestureNavigationEnabled] - final bool? gestureNavigationEnabled; - - @override - String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; - } -} - -/// Configuration to use when creating a new [WebViewPlatformController]. -/// -/// The `autoMediaPlaybackPolicy` parameter must not be null. -class CreationParams { - /// Constructs an instance to use when creating a new - /// [WebViewPlatformController]. - /// - /// The `autoMediaPlaybackPolicy` parameter must not be null. - CreationParams({ - this.initialUrl, - this.webSettings, - this.javascriptChannelNames = const {}, - this.userAgent, - this.autoMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : assert(autoMediaPlaybackPolicy != null); - - /// The initialUrl to load in the webview. - /// - /// When null the webview will be created without loading any page. - final String? initialUrl; - - /// The initial [WebSettings] for the new webview. - /// - /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings? webSettings; - - /// The initial set of JavaScript channels that are configured for this webview. - /// - /// For each value in this set the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - // TODO(amirh): describe what should happen when postMessage is called once that code is migrated - // to PlatformWebView. - final Set javascriptChannelNames; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; - - @override - String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; - } -} - -/// Signature for callbacks reporting that a [WebViewPlatformController] was created. -/// -/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. -typedef WebViewPlatformCreatedCallback = void Function( - WebViewPlatformController? webViewPlatformController); - -/// Interface for a platform implementation of a WebView. -/// -/// [WebView.platform] controls the builder that is used by [WebView]. -/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations -/// for Android and iOS respectively. -abstract class WebViewPlatform { - /// Builds a new WebView. - /// - /// Returns a Widget tree that embeds the created webview. - /// - /// `creationParams` are the initial parameters used to setup the webview. - /// - /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created - /// [WebViewPlatformController]. - /// - /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] - /// implementation is created with the [WebViewPlatformController] instance as a parameter. - /// - /// `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 or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - /// - /// `webViewPlatformHandler` must not be null. - Widget build({ - required BuildContext context, - // TODO(amirh): convert this to be the actual parameters. - // I'm starting without it as the PR is starting to become pretty big. - // I'll followup with the conversion PR. - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }); - - /// Clears all cookies for all [WebView] instances. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() { - throw UnimplementedError( - "WebView clearCookies is not implemented on the current platform"); - } -} +/// Re-export the classes from the webview_flutter_platform_interface through +/// the `platform_interface.dart` file so we don't accidentally break any +/// non-endorsed existing implementations of the interface. +export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + show + AutoMediaPlaybackPolicy, + CreationParams, + JavascriptChannel, + JavascriptChannelRegistry, + JavascriptMessage, + JavascriptMode, + JavascriptMessageHandler, + WebViewPlatform, + WebViewPlatformCallbacksHandler, + WebViewPlatformController, + WebViewPlatformCreatedCallback, + WebSetting, + WebSettings, + WebResourceError, + WebResourceErrorType, + WebViewCookie, + WebViewRequest, + WebViewRequestMethod; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart new file mode 100644 index 000000000000..697eb487b953 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -0,0 +1,834 @@ +// Copyright 2013 The Flutter Authors. 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 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.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); + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return 'NavigationRequest(url: $url, isForMainFrame: $isForMainFrame)'; + } +} + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef NavigationDelegate = FutureOr Function( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef PageStartedCallback = void Function(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef PageFinishedCallback = void Function(String url); + +/// Signature for when a [WebView] is loading a page. +typedef PageLoadingCallback = void Function(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef WebResourceErrorCallback = void Function(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering +/// the `WebView` is not able to block the `WebView` from receiving touch events. +/// See https://github.com/flutter/flutter/issues/53490. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.initialCookies = const [], + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.zoomEnabled = true, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + this.backgroundColor, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + static WebViewPlatform? _platform; + + /// Sets a custom [WebViewPlatform]. + /// + /// This property can be set to use a custom platform implementation for WebViews. + /// + /// Setting `platform` doesn't affect [WebView]s that were already created. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static set platform(WebViewPlatform? platform) { + _platform = platform; + } + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [SurfaceAndroidWebView] on Android and [CupertinoWebView] on iOS. + static WebViewPlatform get platform { + if (_platform == null) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + _platform = SurfaceAndroidWebView(); + break; + case TargetPlatform.iOS: + _platform = CupertinoWebView(); + break; + default: + throw UnsupportedError( + "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); + } + } + return _platform!; + } + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// 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 this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// The initial cookies to set. + final List initialCookies; + + /// Whether JavaScript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(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. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any JavaScript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.runJavascript] or [WebViewController.runJavascriptReturningResult] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// A Boolean value indicating whether the WebView should support zooming + /// using its on-screen zoom controls and gestures. + /// + /// *Note: On iOS [javascriptMode] must be set to + /// [JavascriptMode.unrestricted] in order to set [zoomEnabled] to false + /// + /// By default 'zoomEnabled' is true + final bool zoomEnabled; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + + @override + State createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + + late JavascriptChannelRegistry _javascriptChannelRegistry; + late _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: _onWebViewPlatformCreated, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + javascriptChannelRegistry: _javascriptChannelRegistry, + gestureRecognizers: widget.gestureRecognizers, + creationParams: _creationParamsfromWidget(widget), + ); + } + + @override + void initState() { + super.initState(); + _assertJavascriptChannelNamesAreUnique(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _assertJavascriptChannelNamesAreUnique(); + _controller.future.then((WebViewController controller) { + _platformCallbacksHandler._widget = widget; + controller._updateWidget(widget); + }); + } + + void _onWebViewPlatformCreated(WebViewPlatformController? webViewPlatform) { + final WebViewController controller = WebViewController._( + widget, + webViewPlatform!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + } + + void _assertJavascriptChannelNamesAreUnique() { + if (widget.javascriptChannels == null || + widget.javascriptChannels!.isEmpty) { + return; + } + assert(_extractChannelNames(widget.javascriptChannels).length == + widget.javascriptChannels!.length); + } +} + +CreationParams _creationParamsfromWidget(WebView widget) { + return CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), + userAgent: widget.userAgent, + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, + ); +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + zoomEnabled: widget.zoomEnabled, + ); +} + +// This method assumes that no fields in `currentValue` are null. +WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + assert(newValue.zoomEnabled != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = const WebSetting.absent(); + bool? zoomEnabled; + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + if (currentValue.zoomEnabled != newValue.zoomEnabled) { + zoomEnabled = newValue.zoomEnabled; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + zoomEnabled: zoomEnabled, + ); +} + +Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._widget); + + WebView _widget; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + final NavigationRequest request = + NavigationRequest._(url: url, isForMainFrame: isForMainFrame); + final bool allowNavigation = _widget.navigationDelegate == null || + await _widget.navigationDelegate!(request) == + NavigationDecision.navigate; + return allowNavigation; + } + + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_widget.onPageFinished != null) { + _widget.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_widget.onProgress != null) { + _widget.onProgress!(progress); + } + } + + @override + void onWebResourceError(WebResourceError error) { + if (_widget.onWebResourceError != null) { + _widget.onWebResourceError!(error); + } + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + WebViewController._( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final WebViewPlatformController _webViewPlatformController; + final JavascriptChannelRegistry _javascriptChannelRegistry; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the file located at 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, + ) { + assert(absoluteFilePath.isNotEmpty); + return _webViewPlatformController.loadFile(absoluteFilePath); + } + + /// 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) { + assert(key.isNotEmpty); + return _webViewPlatformController.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 _webViewPlatformController.loadHtmlString( + html, + baseUrl: baseUrl, + ); + } + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Makes a specific HTTP request and 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. + /// + /// Android only: + /// When making a POST request, headers are ignored. As a workaround, make + /// the request manually and load the response data using [loadHTMLString]. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.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. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + Future _updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels(widget.javascriptChannels); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted + /// (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray + /// (e.g '(1,2,3), note that the string for NSArray is formatted and might + /// contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete + /// the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, + /// or on iOS, if the type of the evaluated expression is + /// not supported as described above. + /// + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript + /// embedded in the main frame HTML has been loaded. + @Deprecated('Use [runJavascript] or [runJavascriptReturningResult]') + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When running JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript + /// embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, + /// and returns the result. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted + /// (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray + /// (e.g '(1,2,3), note that the string for NSArray is formatted and might + /// contain newlines and extra spaces.'). + /// + /// 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+. + /// + /// When evaluating JavaScript in a [WebView], it is best practice to wait + /// for the [WebView.onPageFinished] callback. This guarantees all the + /// JavaScript embedded in the main frame HTML has been loaded. + Future runJavascriptReturningResult(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); + } + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } +} + +/// Manages cookies pertaining to all [WebView]s. +class CookieManager { + /// Creates a [CookieManager] -- returns the instance if it's already been called. + factory CookieManager() { + return _instance ??= CookieManager._(); + } + + CookieManager._() { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else if (Platform.isIOS) { + WebViewCookieManagerPlatform.instance = WKWebViewCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported by webview_flutter.'); + } + } + } + + static CookieManager? _instance; + + /// Clears all cookies for all [WebView] instances. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() => + WebViewCookieManagerPlatform.instance!.clearCookies(); + + /// Sets a cookie for all [WebView] instances. + /// + /// This is a no op on iOS versions below 11. + Future setCookie(WebViewCookie cookie) => + WebViewCookieManagerPlatform.instance!.setCookie(cookie); +} + +// Throws an ArgumentError if `url` is not a valid URL string. +void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart deleted file mode 100644 index ca1440d69929..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_android.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an Android webview. -/// -/// This is used as the default implementation for [WebView.platform] on Android. It uses -/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class AndroidWebView implements WebViewPlatform { - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart deleted file mode 100644 index 8d4be3800a28..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_cupertino.dart +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an iOS webview. -/// -/// This is used as the default implementation for [WebView.platform] on iOS. It uses -/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class CupertinoWebView implements WebViewPlatform { - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart deleted file mode 100644 index 05831a9d8794..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_method_channel.dart +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; - -import '../platform_interface.dart'; - -/// A [WebViewPlatformController] that uses a method channel to control the webview. -class MethodChannelWebViewPlatform implements WebViewPlatformController { - /// Constructs an instance that will listen for webviews broadcasting to the - /// given [id], using the given [WebViewPlatformCallbacksHandler]. - MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) - : assert(_platformCallbacksHandler != null), - _channel = MethodChannel('plugins.flutter.io/webview_$id') { - _channel.setMethodCallHandler(_onMethodCall); - } - - final WebViewPlatformCallbacksHandler _platformCallbacksHandler; - - final MethodChannel _channel; - - static const MethodChannel _cookieManagerChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - Future _onMethodCall(MethodCall call) async { - switch (call.method) { - case 'javascriptChannelMessage': - final String channel = call.arguments['channel']!; - final String message = call.arguments['message']!; - _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); - return true; - case 'navigationRequest': - return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url']!, - isForMainFrame: call.arguments['isForMainFrame']!, - ); - case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']!); - return null; - case 'onProgress': - _platformCallbacksHandler.onProgress(call.arguments['progress']); - return null; - case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']!); - return null; - case 'onWebResourceError': - _platformCallbacksHandler.onWebResourceError( - WebResourceError( - errorCode: call.arguments['errorCode']!, - description: call.arguments['description']!, - // iOS doesn't support `failingUrl`. - failingUrl: call.arguments['failingUrl'], - domain: call.arguments['domain'], - errorType: call.arguments['errorType'] == null - ? null - : WebResourceErrorType.values.firstWhere( - (WebResourceErrorType type) { - return type.toString() == - '$WebResourceErrorType.${call.arguments['errorType']}'; - }, - ), - ), - ); - return null; - } - - throw MissingPluginException( - '${call.method} was invoked but has no handler', - ); - } - - @override - Future loadUrl( - String url, - Map? headers, - ) async { - assert(url != null); - return _channel.invokeMethod('loadUrl', { - 'url': url, - 'headers': headers, - }); - } - - @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); - - @override - Future canGoBack() => - _channel.invokeMethod("canGoBack").then((result) => result!); - - @override - Future canGoForward() => - _channel.invokeMethod("canGoForward").then((result) => result!); - - @override - Future goBack() => _channel.invokeMethod("goBack"); - - @override - Future goForward() => _channel.invokeMethod("goForward"); - - @override - Future reload() => _channel.invokeMethod("reload"); - - @override - Future clearCache() => _channel.invokeMethod("clearCache"); - - @override - Future updateSettings(WebSettings settings) async { - final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isNotEmpty) { - await _channel.invokeMethod('updateSettings', updatesMap); - } - } - - @override - Future evaluateJavascript(String javascriptString) { - return _channel - .invokeMethod('evaluateJavascript', javascriptString) - .then((result) => result!); - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'addJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'removeJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future getTitle() => _channel.invokeMethod("getTitle"); - - @override - Future scrollTo(int x, int y) { - return _channel.invokeMethod('scrollTo', { - 'x': x, - 'y': y, - }); - } - - @override - Future scrollBy(int x, int y) { - return _channel.invokeMethod('scrollBy', { - 'x': x, - 'y': y, - }); - } - - @override - Future getScrollX() => - _channel.invokeMethod("getScrollX").then((result) => result!); - - @override - Future getScrollY() => - _channel.invokeMethod("getScrollY").then((result) => result!); - - /// Method channel implementation for [WebViewPlatform.clearCookies]. - static Future clearCookies() { - return _cookieManagerChannel - .invokeMethod('clearCookies') - .then((dynamic result) => result!); - } - - static Map _webSettingsToMap(WebSettings? settings) { - final Map map = {}; - void _addIfNonNull(String key, dynamic value) { - if (value == null) { - return; - } - map[key] = value; - } - - 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( - 'gestureNavigationEnabled', settings.gestureNavigationEnabled); - _addIfNonNull( - 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); - _addSettingIfPresent('userAgent', settings.userAgent); - return map; - } - - /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. - /// - /// This is used for the `creationParams` argument of the platform views created by - /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. - static Map creationParamsToMap( - CreationParams creationParams, { - bool usesHybridComposition = false, - }) { - return { - 'initialUrl': creationParams.initialUrl, - 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), - 'userAgent': creationParams.userAgent, - 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, - 'usesHybridComposition': usesHybridComposition, - }; - } -} diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 398ac876bf3e..ba38771e5107 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -2,833 +2,9 @@ // 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'; +export 'package:webview_flutter_android/webview_android.dart'; +export 'package:webview_flutter_android/webview_surface_android.dart'; +export 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'platform_interface.dart'; -import 'src/webview_android.dart'; -import 'src/webview_cupertino.dart'; -import 'src/webview_method_channel.dart'; - -/// Optional callback invoked when a web view is first created. [controller] is -/// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); - -/// Describes the state of JavaScript support in a given web view. -enum JavascriptMode { - /// JavaScript execution is disabled. - disabled, - - /// JavaScript execution is not restricted. - unrestricted, -} - -/// A message that was sent by JavaScript code running in a [WebView]. -class JavascriptMessage { - /// Constructs a JavaScript message object. - /// - /// The `message` parameter must not be null. - const JavascriptMessage(this.message) : assert(message != null); - - /// The contents of the message that was sent by the JavaScript code. - final String message; -} - -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); - -/// Information about a navigation action that is about to be executed. -class NavigationRequest { - NavigationRequest._({required this.url, required this.isForMainFrame}); - - /// The URL that will be loaded if the navigation is executed. - final String url; - - /// Whether the navigation request is to be loaded as the main frame. - final bool isForMainFrame; - - @override - String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; - } -} - -/// A decision on how to handle a navigation request. -enum NavigationDecision { - /// Prevent the navigation from taking place. - prevent, - - /// Allow the navigation to take place. - navigate, -} - -/// 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 -/// 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 -/// information. -class SurfaceAndroidWebView extends AndroidWebView { - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - }) { - assert(Platform.isAndroid); - assert(webViewPlatformCallbacksHandler != null); - return PlatformViewLink( - viewType: 'plugins.flutter.io/webview', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - 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: MethodChannelWebViewPlatform.creationParamsToMap( - creationParams, - usesHybridComposition: true, - ), - creationParamsCodec: const StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler), - ); - }) - ..create(); - }, - ); - } -} - -/// Decides how to handle a specific navigation request. -/// -/// The returned [NavigationDecision] determines how the navigation described by -/// `navigation` should be handled. -/// -/// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( - NavigationRequest navigation); - -/// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); - -/// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); - -/// Signature for when a [WebView] is loading a page. -typedef void PageLoadingCallback(int progress); - -/// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); - -/// Specifies possible restrictions on automatic media playback. -/// -/// This is typically used in [WebView.initialMediaPlaybackPolicy]. -// The method channel implementation is marshalling this enum to the value's index, so the order -// is important. -enum AutoMediaPlaybackPolicy { - /// Starting any kind of media playback requires a user action. - /// - /// For example: JavaScript code cannot start playing media unless the code was executed - /// as a result of a user action (like a touch event). - require_user_action_for_all_media_types, - - /// Starting any kind of media playback is always allowed. - /// - /// For example: JavaScript code that's triggered when the page is loaded can start playing - /// video or audio. - always_allow, -} - -final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); - -/// A named channel for receiving messaged from JavaScript code running inside a web view. -class JavascriptChannel { - /// Constructs a Javascript channel. - /// - /// The parameters `name` and `onMessageReceived` must not be null. - JavascriptChannel({ - required this.name, - required this.onMessageReceived, - }) : assert(name != null), - assert(onMessageReceived != null), - assert(_validChannelNames.hasMatch(name)); - - /// The channel's name. - /// - /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. - /// - /// The name must start with a letter or underscore(_), followed by any combination of those - /// characters plus digits. - /// - /// Note that any JavaScript existing `window` property with this name will be overriden. - /// - /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. - final String name; - - /// A callback that's invoked when a message is received through the channel. - final JavascriptMessageHandler onMessageReceived; -} - -/// A web view widget for showing html content. -/// -/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering -/// the `WebView` is not able to block the `WebView` from receiving touch events. -/// See https://github.com/flutter/flutter/issues/53490. -class WebView extends StatefulWidget { - /// Creates a new web view. - /// - /// The web view can be controlled using a `WebViewController` that is passed to the - /// `onWebViewCreated` callback once the web view is created. - /// - /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. - const WebView({ - Key? key, - this.onWebViewCreated, - this.initialUrl, - this.javascriptMode = JavascriptMode.disabled, - this.javascriptChannels, - this.navigationDelegate, - this.gestureRecognizers, - this.onPageStarted, - this.onPageFinished, - this.onProgress, - this.onWebResourceError, - this.debuggingEnabled = false, - this.gestureNavigationEnabled = false, - this.userAgent, - this.initialMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - this.allowsInlineMediaPlayback = false, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - assert(allowsInlineMediaPlayback != null), - super(key: key); - - static WebViewPlatform? _platform; - - /// Sets a custom [WebViewPlatform]. - /// - /// This property can be set to use a custom platform implementation for WebViews. - /// - /// Setting `platform` doesn't affect [WebView]s that were already created. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform? platform) { - _platform = platform; - } - - /// The WebView platform that's used by this WebView. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static WebViewPlatform get platform { - if (_platform == null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - _platform = AndroidWebView(); - break; - case TargetPlatform.iOS: - _platform = CupertinoWebView(); - break; - default: - throw UnsupportedError( - "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); - } - } - return _platform!; - } - - /// If not null invoked once the web view is created. - final WebViewCreatedCallback? onWebViewCreated; - - /// 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 this set is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - final Set>? gestureRecognizers; - - /// The initial URL to load. - final String? initialUrl; - - /// Whether Javascript execution is enabled. - final JavascriptMode javascriptMode; - - /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. - /// - /// For each [JavascriptChannel] in the set, a channel object is made available for the - /// JavaScript code in a window property named [JavascriptChannel.name]. - /// The JavaScript code can then call `postMessage` on that object to send a message that will be - /// passed to [JavascriptChannel.onMessageReceived]. - /// - /// For example for the following JavascriptChannel: - /// - /// ```dart - /// JavascriptChannel(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. - /// - /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple - /// channels in the list. - /// - /// A null value is equivalent to an empty set. - final Set? javascriptChannels; - - /// A delegate function that decides how to handle navigation actions. - /// - /// When a navigation is initiated by the WebView (e.g when a user clicks a link) - /// this delegate is called and has to decide how to proceed with the navigation. - /// - /// See [NavigationDecision] for possible decisions the delegate can take. - /// - /// When null all navigation actions are allowed. - /// - /// Caveats on Android: - /// - /// * Navigation actions targeted to the main frame can be intercepted, - /// navigation actions targeted to subframes are allowed regardless of the value - /// returned by this delegate. - /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were - /// triggered by a user gesture, this disables some of Chromium's security mechanisms. - /// A navigationDelegate should only be set when loading trusted content. - /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have - /// a later version): - /// * When a navigationDelegate is set pages with frames are not properly handled by the - /// webview, and frames will be opened in the main frame. - /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate? navigationDelegate; - - /// Controls whether inline playback of HTML5 videos is allowed on iOS. - /// - /// This field is ignored on Android because Android allows it by default. - /// - /// By default `allowsInlineMediaPlayback` is false. - final bool allowsInlineMediaPlayback; - - /// Invoked when a page starts loading. - final PageStartedCallback? onPageStarted; - - /// Invoked when a page has finished loading. - /// - /// This is invoked only for the main frame. - /// - /// When [onPageFinished] is invoked on Android, the page being rendered may - /// not be updated yet. - /// - /// When invoked on iOS or Android, any Javascript code that is embedded - /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback? onPageFinished; - - /// Invoked when a page is loading. - final PageLoadingCallback? onProgress; - - /// Invoked when a web resource has failed to load. - /// - /// This callback is only called for the main page. - final WebResourceErrorCallback? onWebResourceError; - - /// Controls whether WebView debugging is enabled. - /// - /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). - /// - /// WebView debugging is enabled by default in dev builds on iOS. - /// - /// To debug WebViews on iOS: - /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) - /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> - /// - /// By default `debuggingEnabled` is false. - final bool debuggingEnabled; - - /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. - /// - /// This only works on iOS. - /// - /// By default `gestureNavigationEnabled` is false. - final bool gestureNavigationEnabled; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - /// - /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. - /// - /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. - /// - /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom - /// user agent. - /// - /// By default `userAgent` is null. - final String? userAgent; - - /// Which restrictions apply on automatic media playback. - /// - /// This initial value is applied to the platform's webview upon creation. Any following - /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). - /// - /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. - final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; - - @override - State createState() => _WebViewState(); -} - -class _WebViewState extends State { - final Completer _controller = - Completer(); - - late _PlatformCallbacksHandler _platformCallbacksHandler; - - @override - Widget build(BuildContext context) { - return WebView.platform.build( - context: context, - onWebViewPlatformCreated: _onWebViewPlatformCreated, - webViewPlatformCallbacksHandler: _platformCallbacksHandler, - gestureRecognizers: widget.gestureRecognizers, - creationParams: _creationParamsfromWidget(widget), - ); - } - - @override - void initState() { - super.initState(); - _assertJavascriptChannelNamesAreUnique(); - _platformCallbacksHandler = _PlatformCallbacksHandler(widget); - } - - @override - void didUpdateWidget(WebView oldWidget) { - super.didUpdateWidget(oldWidget); - _assertJavascriptChannelNamesAreUnique(); - _controller.future.then((WebViewController controller) { - _platformCallbacksHandler._widget = widget; - controller._updateWidget(widget); - }); - } - - void _onWebViewPlatformCreated(WebViewPlatformController? webViewPlatform) { - final WebViewController controller = WebViewController._( - widget, webViewPlatform!, _platformCallbacksHandler); - _controller.complete(controller); - if (widget.onWebViewCreated != null) { - widget.onWebViewCreated!(controller); - } - } - - void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || - widget.javascriptChannels!.isEmpty) { - return; - } - assert(_extractChannelNames(widget.javascriptChannels).length == - widget.javascriptChannels!.length); - } -} - -CreationParams _creationParamsfromWidget(WebView widget) { - return CreationParams( - initialUrl: widget.initialUrl, - webSettings: _webSettingsFromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), - userAgent: widget.userAgent, - autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, - ); -} - -WebSettings _webSettingsFromWidget(WebView widget) { - return WebSettings( - javascriptMode: widget.javascriptMode, - hasNavigationDelegate: widget.navigationDelegate != null, - hasProgressTracking: widget.onProgress != null, - debuggingEnabled: widget.debuggingEnabled, - gestureNavigationEnabled: widget.gestureNavigationEnabled, - allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, - userAgent: WebSetting.of(widget.userAgent), - ); -} - -// This method assumes that no fields in `currentValue` are null. -WebSettings _clearUnchangedWebSettings( - WebSettings currentValue, WebSettings newValue) { - assert(currentValue.javascriptMode != null); - assert(currentValue.hasNavigationDelegate != null); - assert(currentValue.hasProgressTracking != null); - assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent != null); - assert(newValue.javascriptMode != null); - assert(newValue.hasNavigationDelegate != null); - assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent != null); - - JavascriptMode? javascriptMode; - bool? hasNavigationDelegate; - bool? hasProgressTracking; - bool? debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); - if (currentValue.javascriptMode != newValue.javascriptMode) { - javascriptMode = newValue.javascriptMode; - } - if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { - hasNavigationDelegate = newValue.hasNavigationDelegate; - } - if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { - hasProgressTracking = newValue.hasProgressTracking; - } - if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { - debuggingEnabled = newValue.debuggingEnabled; - } - if (currentValue.userAgent != newValue.userAgent) { - userAgent = newValue.userAgent; - } - - return WebSettings( - javascriptMode: javascriptMode, - hasNavigationDelegate: hasNavigationDelegate, - hasProgressTracking: hasProgressTracking, - debuggingEnabled: debuggingEnabled, - userAgent: userAgent, - ); -} - -Set _extractChannelNames(Set? channels) { - final Set channelNames = channels == null - ? {} - : channels.map((JavascriptChannel channel) => channel.name).toSet(); - return channelNames; -} - -class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { - _PlatformCallbacksHandler(this._widget) { - _updateJavascriptChannelsFromSet(_widget.javascriptChannels); - } - - WebView _widget; - - // Maps a channel name to a channel. - final Map _javascriptChannels = - {}; - - @override - void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel]!.onMessageReceived(JavascriptMessage(message)); - } - - @override - FutureOr onNavigationRequest({ - required String url, - required bool isForMainFrame, - }) async { - final NavigationRequest request = - NavigationRequest._(url: url, isForMainFrame: isForMainFrame); - final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate!(request) == - NavigationDecision.navigate; - return allowNavigation; - } - - @override - void onPageStarted(String url) { - if (_widget.onPageStarted != null) { - _widget.onPageStarted!(url); - } - } - - @override - void onPageFinished(String url) { - if (_widget.onPageFinished != null) { - _widget.onPageFinished!(url); - } - } - - @override - void onProgress(int progress) { - if (_widget.onProgress != null) { - _widget.onProgress!(progress); - } - } - - void onWebResourceError(WebResourceError error) { - if (_widget.onWebResourceError != null) { - _widget.onWebResourceError!(error); - } - } - - void _updateJavascriptChannelsFromSet(Set? channels) { - _javascriptChannels.clear(); - if (channels == null) { - return; - } - for (JavascriptChannel channel in channels) { - _javascriptChannels[channel.name] = channel; - } - } -} - -/// Controls a [WebView]. -/// -/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] -/// callback for a [WebView] widget. -class WebViewController { - WebViewController._( - this._widget, - this._webViewPlatformController, - this._platformCallbacksHandler, - ) : assert(_webViewPlatformController != null) { - _settings = _webSettingsFromWidget(_widget); - } - - final WebViewPlatformController _webViewPlatformController; - - final _PlatformCallbacksHandler _platformCallbacksHandler; - - late WebSettings _settings; - - WebView _widget; - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, { - Map? headers, - }) async { - assert(url != null); - _validateUrlString(url); - return _webViewPlatformController.loadUrl(url, headers); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If [WebView.initialUrl] was never specified, returns `null`. - /// Note that this operation is asynchronous, and it is possible that the - /// current URL changes again by the time this function returns (in other - /// words, by the time this future completes, the WebView may be displaying a - /// different URL). - Future currentUrl() { - return _webViewPlatformController.currentUrl(); - } - - /// Checks whether there's a back history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has - /// changed by the time the future completed. - Future canGoBack() { - return _webViewPlatformController.canGoBack(); - } - - /// Checks whether there's a forward history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has - /// changed by the time the future completed. - Future canGoForward() { - return _webViewPlatformController.canGoForward(); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - return _webViewPlatformController.goBack(); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - return _webViewPlatformController.goForward(); - } - - /// Reloads the current URL. - Future reload() { - return _webViewPlatformController.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. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - /// - /// Note: Calling this method also triggers a reload. - Future clearCache() async { - await _webViewPlatformController.clearCache(); - return reload(); - } - - Future _updateWidget(WebView widget) async { - _widget = widget; - await _updateSettings(_webSettingsFromWidget(widget)); - await _updateJavascriptChannels(widget.javascriptChannels); - } - - Future _updateSettings(WebSettings newSettings) { - final WebSettings update = - _clearUnchangedWebSettings(_settings, newSettings); - _settings = newSettings; - return _webViewPlatformController.updateSettings(update); - } - - Future _updateJavascriptChannels( - Set? newChannels) async { - final Set currentChannels = - _platformCallbacksHandler._javascriptChannels.keys.toSet(); - final Set newChannelNames = _extractChannelNames(newChannels); - final Set channelsToAdd = - newChannelNames.difference(currentChannels); - final Set channelsToRemove = - currentChannels.difference(newChannelNames); - if (channelsToRemove.isNotEmpty) { - await _webViewPlatformController - .removeJavascriptChannels(channelsToRemove); - } - if (channelsToAdd.isNotEmpty) { - await _webViewPlatformController.addJavascriptChannels(channelsToAdd); - } - _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: - /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. - /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. - /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript - /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { - if (_settings.javascriptMode == JavascriptMode.disabled) { - return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); - } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - return _webViewPlatformController.getTitle(); - } - - /// Sets the WebView's content scroll position. - /// - /// The parameters `x` and `y` specify the scroll position in WebView pixels. - Future scrollTo(int x, int y) { - return _webViewPlatformController.scrollTo(x, y); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. - Future scrollBy(int x, int y) { - return _webViewPlatformController.scrollBy(x, y); - } - - /// Return the horizontal scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - return _webViewPlatformController.getScrollX(); - } - - /// Return the vertical scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - return _webViewPlatformController.getScrollY(); - } -} - -/// Manages cookies pertaining to all [WebView]s. -class CookieManager { - /// Creates a [CookieManager] -- returns the instance if it's already been called. - factory CookieManager() { - return _instance ??= CookieManager._(); - } - - CookieManager._(); - - static CookieManager? _instance; - - /// Clears all cookies for all [WebView] instances. - /// - /// This is a no op on iOS version smaller than 9. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => WebView.platform.clearCookies(); -} - -// Throws an ArgumentError if `url` is not a valid URL string. -void _validateUrlString(String url) { - try { - final Uri uri = Uri.parse(url); - if (uri.scheme.isEmpty) { - throw ArgumentError('Missing scheme in URL string: "$url"'); - } - } on FormatException catch (e) { - throw ArgumentError(e); - } -} +export 'platform_interface.dart'; +export 'src/webview.dart'; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 393a66e3f92e..fc1e50f16e24 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -1,29 +1,33 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter +repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.14 +version: 3.0.4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: platforms: android: - package: io.flutter.plugins.webviewflutter - pluginClass: WebViewFlutterPlugin + default_package: webview_flutter_android ios: - pluginClass: FLTWebViewFlutterPlugin + default_package: webview_flutter_wkwebview dependencies: flutter: sdk: flutter + webview_flutter_android: ^2.8.0 + webview_flutter_platform_interface: ^1.8.0 + webview_flutter_wkwebview: ^2.7.0 dev_dependencies: + build_runner: ^2.1.5 flutter_driver: sdk: flutter flutter_test: sdk: flutter + mockito: ^5.0.16 pedantic: ^1.10.0 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 5efee6d9952d..d7189917c221 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -2,37 +2,57 @@ // 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/services.dart'; import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter/platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -typedef void VoidCallback(); +import 'webview_flutter_test.mocks.dart'; +typedef VoidCallback = void Function(); + +@GenerateMocks([WebViewPlatform, WebViewPlatformController]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final _FakePlatformViewsController fakePlatformViewsController = - _FakePlatformViewsController(); + late MockWebViewPlatform mockWebViewPlatform; + late MockWebViewPlatformController mockWebViewPlatformController; + late MockWebViewCookieManagerPlatform mockWebViewCookieManagerPlatform; - final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); + setUp(() { + mockWebViewPlatformController = MockWebViewPlatformController(); + mockWebViewPlatform = MockWebViewPlatform(); + mockWebViewCookieManagerPlatform = MockWebViewCookieManagerPlatform(); + when(mockWebViewPlatform.build( + context: anyNamed('context'), + creationParams: anyNamed('creationParams'), + webViewPlatformCallbacksHandler: + anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: anyNamed('gestureRecognizers'), + )).thenAnswer((Invocation invocation) { + final WebViewPlatformCreatedCallback onWebViewPlatformCreated = + invocation.namedArguments[const Symbol('onWebViewPlatformCreated')] + as WebViewPlatformCreatedCallback; + return TestPlatformWebView( + mockWebViewPlatformController: mockWebViewPlatformController, + onWebViewPlatformCreated: onWebViewPlatformCreated, + ); + }); - setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); - SystemChannels.platform - .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); + WebView.platform = mockWebViewPlatform; + WebViewCookieManagerPlatform.instance = mockWebViewCookieManagerPlatform; }); - setUp(() { - fakePlatformViewsController.reset(); - _fakeCookieManager.reset(); + tearDown(() { + mockWebViewCookieManagerPlatform.reset(); }); testWidgets('Create WebView', (WidgetTester tester) async { @@ -40,38 +60,44 @@ void main() { }); testWidgets('Initial url', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); + await tester.pumpWidget(const WebView(initialUrl: 'https://youtube.com')); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(await controller.currentUrl(), 'https://youtube.com'); + expect(params.initialUrl, 'https://youtube.com'); }); testWidgets('Javascript mode', (WidgetTester tester) async { await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams unrestrictedparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); + expect( + unrestrictedparams.webSettings!.javascriptMode, + JavascriptMode.unrestricted, + ); await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.disabled, )); - expect(platformWebView.javascriptMode, JavascriptMode.disabled); + + final CreationParams disabledparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled); }); - testWidgets('Load url', (WidgetTester tester) async { + testWidgets('Load file', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -83,12 +109,14 @@ void main() { expect(controller, isNotNull); - await controller!.loadUrl('https://flutter.io'); + await controller!.loadFile('/test/path/index.html'); - expect(await controller!.currentUrl(), 'https://flutter.io'); + verify(mockWebViewPlatformController.loadFile( + '/test/path/index.html', + )); }); - testWidgets('Invalid urls', (WidgetTester tester) async { + testWidgets('Load file with empty path', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -100,17 +128,29 @@ void main() { expect(controller, isNotNull); - expect(await controller!.currentUrl(), isNull); + expect(() => controller!.loadFile(''), throwsAssertionError); + }); - expect(() => controller!.loadUrl(''), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); + testWidgets('Load Flutter asset', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); - // Missing schema. - expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); + expect(controller, isNotNull); + + await controller!.loadFlutterAsset('assets/index.html'); + + verify(mockWebViewPlatformController.loadFlutterAsset( + 'assets/index.html', + )); }); - testWidgets('Headers in loadUrl', (WidgetTester tester) async { + testWidgets('Load Flutter asset with empty key', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -122,15 +162,10 @@ void main() { expect(controller, isNotNull); - final Map headers = { - 'CACHE-CONTROL': 'ABC' - }; - await controller!.loadUrl('https://flutter.io', headers: headers); - expect(await controller!.currentUrl(), equals('https://flutter.io')); + expect(() => controller!.loadFlutterAsset(''), throwsAssertionError); }); - testWidgets("Can't go back before loading a page", - (WidgetTester tester) async { + testWidgets('Load HTML string without base URL', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -142,12 +177,14 @@ void main() { expect(controller, isNotNull); - final bool canGoBackNoPageLoaded = await controller!.canGoBack(); + await controller!.loadHtmlString('

This is a test paragraph.

'); - expect(canGoBackNoPageLoaded, false); + verify(mockWebViewPlatformController.loadHtmlString( + '

This is a test paragraph.

', + )); }); - testWidgets("Clear Cache", (WidgetTester tester) async { + testWidgets('Load HTML string with base URL', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -158,18 +195,23 @@ void main() { ); expect(controller, isNotNull); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, true); - await controller!.clearCache(); + await controller!.loadHtmlString( + '

This is a test paragraph.

', + baseUrl: 'https://flutter.dev', + ); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, false); + verify(mockWebViewPlatformController.loadHtmlString( + '

This is a test paragraph.

', + baseUrl: 'https://flutter.dev', + )); }); - testWidgets("Can't go back with no history", (WidgetTester tester) async { + testWidgets('Load HTML string with empty string', + (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://flutter.io', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -177,16 +219,14 @@ void main() { ); expect(controller, isNotNull); - final bool canGoBackFirstPageLoaded = await controller!.canGoBack(); - expect(canGoBackFirstPageLoaded, false); + expect(() => controller!.loadHtmlString(''), throwsAssertionError); }); - testWidgets('Can go back', (WidgetTester tester) async { + testWidgets('Load url', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://flutter.io', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -195,14 +235,15 @@ void main() { expect(controller, isNotNull); - await controller!.loadUrl('https://www.google.com'); - final bool canGoBackSecondPageLoaded = await controller!.canGoBack(); + await controller!.loadUrl('https://flutter.io'); - expect(canGoBackSecondPageLoaded, true); + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + argThat(isNull), + )); }); - testWidgets("Can't go forward before loading a page", - (WidgetTester tester) async { + testWidgets('Invalid urls', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -214,16 +255,21 @@ void main() { expect(controller, isNotNull); - final bool canGoForwardNoPageLoaded = await controller!.canGoForward(); + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.initialUrl, isNull); - expect(canGoForwardNoPageLoaded, false); + expect(() => controller!.loadUrl(''), throwsA(anything)); + expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); }); - testWidgets("Can't go forward with no history", (WidgetTester tester) async { + testWidgets('Headers in loadUrl', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://flutter.io', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -231,36 +277,45 @@ void main() { ); expect(controller, isNotNull); - final bool canGoForwardFirstPageLoaded = await controller!.canGoForward(); - expect(canGoForwardFirstPageLoaded, false); + final Map headers = { + 'CACHE-CONTROL': 'ABC' + }; + await controller!.loadUrl('https://flutter.io', headers: headers); + + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + {'CACHE-CONTROL': 'ABC'}, + )); }); - testWidgets('Can go forward', (WidgetTester tester) async { + testWidgets('loadRequest', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://flutter.io', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, ), ); - expect(controller, isNotNull); - await controller!.loadUrl('https://youtube.com'); - await controller!.goBack(); - final bool canGoForwardFirstPageBacked = await controller!.canGoForward(); + final WebViewRequest req = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + + await controller!.loadRequest(req); - expect(canGoForwardFirstPageBacked, true); + verify(mockWebViewPlatformController.loadRequest(req)); }); - testWidgets('Go back', (WidgetTester tester) async { + testWidgets('Clear Cache', (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://youtube.com', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -269,22 +324,36 @@ void main() { expect(controller, isNotNull); - expect(await controller!.currentUrl(), 'https://youtube.com'); + await controller!.clearCache(); - await controller!.loadUrl('https://flutter.io'); + verify(mockWebViewPlatformController.clearCache()); + }); - expect(await controller!.currentUrl(), 'https://flutter.io'); + testWidgets('Can go back', (WidgetTester tester) async { + when(mockWebViewPlatformController.canGoBack()) + .thenAnswer((_) => Future.value(true)); - await controller!.goBack(); + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); - expect(await controller!.currentUrl(), 'https://youtube.com'); + expect(controller, isNotNull); + expect(controller!.canGoBack(), completion(true)); }); - testWidgets('Go forward', (WidgetTester tester) async { + testWidgets("Can't go forward", (WidgetTester tester) async { + when(mockWebViewPlatformController.canGoForward()) + .thenAnswer((_) => Future.value(false)); + WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://youtube.com', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -292,23 +361,44 @@ void main() { ); expect(controller, isNotNull); + expect(controller!.canGoForward(), completion(false)); + }); - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); + testWidgets('Go back', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); await controller!.goBack(); + verify(mockWebViewPlatformController.goBack()); + }); - expect(await controller!.currentUrl(), 'https://youtube.com'); + testWidgets('Go forward', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(controller, isNotNull); await controller!.goForward(); - - expect(await controller!.currentUrl(), 'https://flutter.io'); + verify(mockWebViewPlatformController.goForward()); }); testWidgets('Current URL', (WidgetTester tester) async { + when(mockWebViewPlatformController.currentUrl()) + .thenAnswer((_) => Future.value('https://youtube.com')); + WebViewController? controller; await tester.pumpWidget( WebView( @@ -319,17 +409,6 @@ void main() { ); expect(controller, isNotNull); - - // Test a WebView without an explicitly set first URL. - expect(await controller!.currentUrl(), isNull); - - await controller!.loadUrl('https://youtube.com'); - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); expect(await controller!.currentUrl(), 'https://youtube.com'); }); @@ -344,23 +423,14 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - await controller.reload(); - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); - - await controller.loadUrl('https://youtube.com'); - - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + verify(mockWebViewPlatformController.reload()); }); testWidgets('evaluate Javascript', (WidgetTester tester) async { + when(mockWebViewPlatformController.evaluateJavascript('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + late WebViewController controller; await tester.pumpWidget( WebView( @@ -371,8 +441,11 @@ void main() { }, ), ); + expect( - await controller.evaluateJavascript("fake js string"), "fake js string", + // ignore: deprecated_member_use_from_same_package + await controller.evaluateJavascript('fake js string'), + 'fake js string', reason: 'should get the argument'); }); @@ -389,11 +462,83 @@ void main() { ), ); expect( + // ignore: deprecated_member_use_from_same_package () => controller.evaluateJavascript('fake js string'), throwsA(anything), ); }); + testWidgets('runJavaScript', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + await controller.runJavascript('fake js string'); + verify(mockWebViewPlatformController.runJavascript('fake js string')); + }); + + testWidgets('runJavaScript with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascript('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + when(mockWebViewPlatformController + .runJavascriptReturningResult('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(await controller.runJavascriptReturningResult('fake js string'), + 'fake js string', + reason: 'should get the argument'); + }); + + testWidgets('runJavaScriptReturningResult with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascriptReturningResult('fake js string'), + throwsA(anything), + ); + }); + testWidgets('Cookies can be cleared once', (WidgetTester tester) async { await tester.pumpWidget( const WebView( @@ -405,18 +550,19 @@ void main() { expect(hasCookies, true); }); - testWidgets('Second cookie clear does not have cookies', - (WidgetTester tester) async { + testWidgets('Cookies can be set', (WidgetTester tester) async { + const WebViewCookie cookie = + WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'); + await tester.pumpWidget( const WebView( initialUrl: 'https://flutter.io', ), ); final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - final bool hasCookiesSecond = await cookieManager.clearCookies(); - expect(hasCookiesSecond, false); + await cookieManager.setCookie(cookie); + expect(mockWebViewCookieManagerPlatform.setCookieCalls, + [cookie]); }); testWidgets('Initial JavaScript channels', (WidgetTester tester) async { @@ -432,15 +578,17 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(platformWebView.javascriptChannelNames, + expect(params.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm'])); }); 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); @@ -499,11 +647,15 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).first as JavascriptChannelRegistry; - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + expect( + channelRegistry.channels.keys, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3']), + ); }); testWidgets('Remove all JavaScript channels and then add', @@ -538,11 +690,12 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).last as JavascriptChannelRegistry; - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts'])); + expect(channelRegistry.channels.keys, unorderedEquals(['Tts'])); }); testWidgets('JavaScript channel messages', (WidgetTester tester) async { @@ -566,14 +719,16 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).single as JavascriptChannelRegistry; expect(ttsMessagesReceived, isEmpty); expect(alarmMessagesReceived, isEmpty); - platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); - platformWebView.fakeJavascriptPostMessage('Tts', 'World'); + channelRegistry.onJavascriptChannelMessage('Tts', 'Hello'); + channelRegistry.onJavascriptChannelMessage('Tts', 'World'); expect(ttsMessagesReceived, ['Hello', 'World']); }); @@ -589,12 +744,14 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; - platformWebView.fakeOnPageStartedCallback(); + handler.onPageStarted('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); testWidgets('onPageStarted is null', (WidgetTester tester) async { @@ -603,12 +760,14 @@ void main() { onPageStarted: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; // The platform side will always invoke a call for onPageStarted. This is // to test that it does not crash on a null callback. - platformWebView.fakeOnPageStartedCallback(); + handler.onPageStarted('https://youtube.com'); }); testWidgets('onPageStarted changed', (WidgetTester tester) async { @@ -626,12 +785,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageStartedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last as WebViewPlatformCallbacksHandler; + handler.onPageStarted('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); }); @@ -646,12 +806,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageFinishedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + handler.onPageFinished('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); testWidgets('onPageFinished is null', (WidgetTester tester) async { @@ -660,12 +821,13 @@ void main() { onPageFinished: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; // The platform side will always invoke a call for onPageFinished. This is // to test that it does not crash on a null callback. - platformWebView.fakeOnPageFinishedCallback(); + handler.onPageFinished('https://youtube.com'); }); testWidgets('onPageFinished changed', (WidgetTester tester) async { @@ -683,12 +845,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageFinishedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last as WebViewPlatformCallbacksHandler; + handler.onPageFinished('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); }); @@ -703,10 +866,11 @@ void main() { }, )); - final FakePlatformWebView? platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView?.fakeOnProgressCallback(50); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + handler.onProgress(50); expect(loadingProgress, 50); }); @@ -717,11 +881,13 @@ void main() { onProgress: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; // This is to test that it does not crash on a null callback. - platformWebView.fakeOnProgressCallback(50); + handler.onProgress(50); }); testWidgets('onLoadingProgress changed', (WidgetTester tester) async { @@ -739,10 +905,11 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnProgressCallback(50); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last as WebViewPlatformCallbacksHandler; + handler.onProgress(50); expect(loadingProgress, 50); }); @@ -754,10 +921,12 @@ void main() { initialUrl: 'https://youtube.com', )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(platformWebView.hasNavigationDelegate, false); + expect(params.webSettings!.hasNavigationDelegate, false); await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -765,7 +934,12 @@ void main() { NavigationDecision.navigate, )); - expect(platformWebView.hasNavigationDelegate, true); + final WebSettings updateSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .single as WebSettings; + + expect(updateSettings.hasNavigationDelegate, true); }); testWidgets('Block navigation', (WidgetTester tester) async { @@ -781,22 +955,39 @@ void main() { : NavigationDecision.prevent; })); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final List args = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + webViewPlatformCallbacksHandler: true, + ); + + final CreationParams params = args[0] as CreationParams; + expect(params.webSettings!.hasNavigationDelegate, true); - expect(platformWebView.hasNavigationDelegate, true); + final WebViewPlatformCallbacksHandler handler = + args[1] as WebViewPlatformCallbacksHandler; - platformWebView.fakeNavigate('https://www.google.com'); // The navigation delegate only allows navigation to https://flutter.dev // so we should still be in https://youtube.com. - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect( + handler.onNavigationRequest( + url: 'https://www.google.com', + isForMainFrame: true, + ), + completion(false), + ); + expect(navigationRequests.length, 1); expect(navigationRequests[0].url, 'https://www.google.com'); expect(navigationRequests[0].isForMainFrame, true); - platformWebView.fakeNavigate('https://flutter.dev'); - await tester.pump(); - expect(platformWebView.currentUrl, 'https://flutter.dev'); + expect( + handler.onNavigationRequest( + url: 'https://flutter.dev', + isForMainFrame: true, + ), + completion(true), + ); }); }); @@ -806,46 +997,137 @@ void main() { debuggingEnabled: true, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(platformWebView.debuggingEnabled, true); + expect(params.webSettings!.debuggingEnabled, true); }); testWidgets('defaults to false', (WidgetTester tester) async { await tester.pumpWidget(const WebView()); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(platformWebView.debuggingEnabled, false); + expect(params.webSettings!.debuggingEnabled, false); }); testWidgets('can be changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(WebView(key: key)); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - await tester.pumpWidget(WebView( key: key, debuggingEnabled: true, )); - expect(platformWebView.debuggingEnabled, true); + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(enabledSettings.debuggingEnabled, true); await tester.pumpWidget(WebView( key: key, debuggingEnabled: false, )); - expect(platformWebView.debuggingEnabled, false); + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(disabledSettings.debuggingEnabled, false); + }); + }); + + group('zoomEnabled', () { + testWidgets('Enable zoom', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + zoomEnabled: true, + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.zoomEnabled, isTrue); + }); + + testWidgets('defaults to true', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.zoomEnabled, isTrue); + }); + + testWidgets('can be changed', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(WebView(key: key)); + + await tester.pumpWidget(WebView( + key: key, + zoomEnabled: true, + )); + + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + // Zoom defaults to true, so no changes are made to settings. + expect(enabledSettings.zoomEnabled, isNull); + + await tester.pumpWidget(WebView( + key: key, + zoomEnabled: false, + )); + + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(disabledSettings.zoomEnabled, isFalse); + }); + }); + + group('Background color', () { + testWidgets('Defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.backgroundColor, null); + }); + + testWidgets('Can be transparent', (WidgetTester tester) async { + const Color transparentColor = Color(0x00000000); + + await tester.pumpWidget(const WebView( + backgroundColor: transparentColor, + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.backgroundColor, transparentColor); }); }); group('Custom platform implementation', () { - setUpAll(() { + setUp(() { WebView.platform = MyWebViewPlatform(); }); tearDownAll(() { @@ -871,8 +1153,9 @@ void main() { javascriptMode: JavascriptMode.disabled, hasNavigationDelegate: false, debuggingEnabled: false, - userAgent: WebSetting.of(null), + userAgent: const WebSetting.of(null), gestureNavigationEnabled: true, + zoomEnabled: true, ), ))); }); @@ -901,16 +1184,19 @@ void main() { expect(platform.lastRequestHeaders, headers); }); }); + testWidgets('Set UserAgent', (WidgetTester tester) async { await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; - expect(platformWebView.userAgent, isNull); + expect(params.webSettings!.userAgent.value, isNull); await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', @@ -918,252 +1204,74 @@ void main() { userAgent: 'UA', )); - expect(platformWebView.userAgent, 'UA'); + final WebSettings settings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(settings.userAgent.value, 'UA'); }); } -class FakePlatformWebView { - FakePlatformWebView(int? id, Map params) { - if (params.containsKey('initialUrl')) { - final String? initialUrl = params['initialUrl']; - if (initialUrl != null) { - history.add(initialUrl); - currentPosition++; - } - } - if (params.containsKey('javascriptChannelNames')) { - javascriptChannelNames = - List.from(params['javascriptChannelNames']); - } - javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; - hasNavigationDelegate = - params['settings']['hasNavigationDelegate'] ?? false; - debuggingEnabled = params['settings']['debuggingEnabled']; - userAgent = params['settings']['userAgent']; - channel = MethodChannel( - 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); - channel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel channel; - - List history = []; - int currentPosition = -1; - int amountOfReloadsOnCurrentUrl = 0; - bool hasCache = true; - - String? get currentUrl => history.isEmpty ? null : history[currentPosition]; - JavascriptMode? javascriptMode; - List? javascriptChannelNames; - - bool? hasNavigationDelegate; - bool? debuggingEnabled; - String? userAgent; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'loadUrl': - final Map request = call.arguments; - _loadUrl(request['url']); - return Future.sync(() {}); - case 'updateSettings': - if (call.arguments['jsMode'] != null) { - javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; - } - if (call.arguments['hasNavigationDelegate'] != null) { - hasNavigationDelegate = call.arguments['hasNavigationDelegate']; - } - if (call.arguments['debuggingEnabled'] != null) { - debuggingEnabled = call.arguments['debuggingEnabled']; - } - userAgent = call.arguments['userAgent']; - break; - case 'canGoBack': - return Future.sync(() => currentPosition > 0); - case 'canGoForward': - return Future.sync(() => currentPosition < history.length - 1); - case 'goBack': - currentPosition = max(-1, currentPosition - 1); - return Future.sync(() {}); - case 'goForward': - currentPosition = min(history.length - 1, currentPosition + 1); - return Future.sync(() {}); - case 'reload': - amountOfReloadsOnCurrentUrl++; - return Future.sync(() {}); - case 'currentUrl': - return Future.value(currentUrl); - case 'evaluateJavascript': - return Future.value(call.arguments); - case 'addJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames!.addAll(channelNames); - break; - case 'removeJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames! - .removeWhere((String channel) => channelNames.contains(channel)); - break; - case 'clearCache': - hasCache = false; - return Future.sync(() {}); - } - return Future.sync(() {}); - } - - void fakeJavascriptPostMessage(String jsChannel, String message) { - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'channel': jsChannel, - 'message': message - }; - final ByteData data = codec - .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - // Fakes a main frame navigation that was initiated by the webview, e.g when - // the user clicks a link in the currently loaded page. - void fakeNavigate(String url) { - if (!hasNavigationDelegate!) { - print('no navigation delegate'); - _loadUrl(url); - return; - } - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'url': url, - 'isForMainFrame': true - }; - final ByteData data = - codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) { - final bool allow = codec.decodeEnvelope(data!); - if (allow) { - _loadUrl(url); - } - }); - } - - void fakeOnPageStartedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageStarted', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnPageFinishedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageFinished', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnProgressCallback(int progress) { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onProgress', - {'progress': progress}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - void _loadUrl(String? url) { - history = history.sublist(0, currentPosition + 1); - history.add(url); - currentPosition++; - amountOfReloadsOnCurrentUrl = 0; - } +List captureBuildArgs( + MockWebViewPlatform mockWebViewPlatform, { + bool context = false, + bool creationParams = false, + bool webViewPlatformCallbacksHandler = false, + bool javascriptChannelRegistry = false, + bool onWebViewPlatformCreated = false, + bool gestureRecognizers = false, +}) { + return verify(mockWebViewPlatform.build( + context: context ? captureAnyNamed('context') : anyNamed('context'), + creationParams: creationParams + ? captureAnyNamed('creationParams') + : anyNamed('creationParams'), + webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler + ? captureAnyNamed('webViewPlatformCallbacksHandler') + : anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: javascriptChannelRegistry + ? captureAnyNamed('javascriptChannelRegistry') + : anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: onWebViewPlatformCreated + ? captureAnyNamed('onWebViewPlatformCreated') + : anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: gestureRecognizers + ? captureAnyNamed('gestureRecognizers') + : anyNamed('gestureRecognizers'), + )).captured; } -class _FakePlatformViewsController { - FakePlatformWebView? lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = call.arguments; - final Map params = _decodeParams(args['params'])!; - lastCreatedView = FakePlatformWebView( - args['id'], - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } +// This Widget ensures that onWebViewPlatformCreated is only called once when +// making multiple calls to `WidgetTester.pumpWidget` with different parameters +// for the WebView. +class TestPlatformWebView extends StatefulWidget { + const TestPlatformWebView({ + Key? key, + required this.mockWebViewPlatformController, + this.onWebViewPlatformCreated, + }) : super(key: key); - void reset() { - lastCreatedView = null; - } -} + final MockWebViewPlatformController mockWebViewPlatformController; + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; -Map? _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, - ); - return const StandardMessageCodec().decodeMessage(messageBytes); + @override + State createState() => TestPlatformWebViewState(); } -class _FakeCookieManager { - _FakeCookieManager() { - final MethodChannel channel = const MethodChannel( - 'plugins.flutter.io/cookie_manager', - StandardMethodCodec(), - ); - channel.setMockMethodCallHandler(onMethodCall); - } - - bool hasCookies = true; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'clearCookies': - bool hadCookies = false; - if (hasCookies) { - hadCookies = true; - hasCookies = false; - } - return Future.sync(() { - return hadCookies; - }); +class TestPlatformWebViewState extends State { + @override + void initState() { + super.initState(); + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated = + widget.onWebViewPlatformCreated; + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(widget.mockWebViewPlatformController); } - return Future.sync(() => true); } - void reset() { - hasCookies = true; + @override + Widget build(BuildContext context) { + return Container(); } } @@ -1175,6 +1283,7 @@ class MyWebViewPlatform implements WebViewPlatform { BuildContext? context, CreationParams? creationParams, required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, WebViewPlatformCreatedCallback? onWebViewPlatformCreated, Set>? gestureRecognizers, }) { @@ -1228,7 +1337,8 @@ class MatchesWebSettings extends Matcher { _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && _webSettings!.gestureNavigationEnabled == webSettings.gestureNavigationEnabled && - _webSettings!.userAgent == webSettings.userAgent; + _webSettings!.userAgent == webSettings.userAgent && + _webSettings!.zoomEnabled == webSettings.zoomEnabled; } } @@ -1252,9 +1362,18 @@ class MatchesCreationParams extends Matcher { } } -/// 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; +class MockWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform { + List setCookieCalls = []; + + @override + Future clearCookies() async => true; + + @override + Future setCookie(WebViewCookie cookie) async { + setCookieCalls.add(cookie); + } + + void reset() { + setCookieCalls = []; + } +} 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 new file mode 100644 index 000000000000..fe3060164df2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.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. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter/test/webview_flutter_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i9; + +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/widgets.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart' + as _i7; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform.dart' + as _i4; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_controller.dart' + as _i10; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i5; + +// 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 _FakeWidget_0 extends _i1.Fake implements _i2.Widget { + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Widget build( + {_i2.BuildContext? context, + _i5.CreationParams? creationParams, + _i6.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + _i7.JavascriptChannelRegistry? javascriptChannelRegistry, + _i4.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i3.Factory<_i8.OneSequenceGestureRecognizer>>? + gestureRecognizers}) => + (super.noSuchMethod( + Invocation.method(#build, [], { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers + }), + returnValue: _FakeWidget_0()) as _i2.Widget); + @override + _i9.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i9.Future); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewPlatformController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformController extends _i1.Mock + implements _i10.WebViewPlatformController { + MockWebViewPlatformController() { + _i1.throwOnMissingStub(this); + } + + @override + _i9.Future loadFile(String? absoluteFilePath) => + (super.noSuchMethod(Invocation.method(#loadFile, [absoluteFilePath]), + returnValue: Future.value(), + returnValueForMissingStub: 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); + @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); + @override + _i9.Future loadUrl(String? url, Map? headers) => + (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), + returnValue: Future.value(), + returnValueForMissingStub: 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); + @override + _i9.Future updateSettings(_i5.WebSettings? setting) => + (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future currentUrl() => + (super.noSuchMethod(Invocation.method(#currentUrl, []), + returnValue: Future.value()) as _i9.Future); + @override + _i9.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future clearCache() => + (super.noSuchMethod(Invocation.method(#clearCache, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future evaluateJavascript(String? javascript) => + (super.noSuchMethod(Invocation.method(#evaluateJavascript, [javascript]), + returnValue: 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); + @override + _i9.Future runJavascriptReturningResult(String? javascript) => + (super.noSuchMethod( + Invocation.method(#runJavascriptReturningResult, [javascript]), + returnValue: 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); + @override + _i9.Future removeJavascriptChannels( + Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #removeJavascriptChannels, [javascriptChannelNames]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: 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); + @override + _i9.Future scrollBy(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future getScrollX() => + (super.noSuchMethod(Invocation.method(#getScrollX, []), + returnValue: 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(); +} diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS index 4461b602a13b..22e2b0ef78fc 100644 --- a/packages/webview_flutter/webview_flutter_android/AUTHORS +++ b/packages/webview_flutter/webview_flutter_android/AUTHORS @@ -65,4 +65,5 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Maurits van Beusekom +Nick Bradshaw diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index d6a10e9b918a..7261cc1c2cb5 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,4 +1,120 @@ +## NEXT + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 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 + +* Fixes a bug causing `debuggingEnabled` to always be set to true. +* Fixes an integration test race condition. + +## 2.8.2 + +* Adds the `WebSettings.setAllowFileAccess()` method and ensure that file access is allowed when the `WebViewAndroidWidget.loadFile()` method is executed. + +## 2.8.1 + +* Fixes bug where the default user agent string was being set for every rebuild. See + https://github.com/flutter/flutter/issues/94847. + +## 2.8.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + +## 2.7.0 + +* Adds support for the `loadRequest` method from the platform interface. + +## 2.6.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + +## 2.5.0 + +* Adds an option to set the background color of the webview. + +## 2.4.0 + +* Adds support for Android's `WebView.loadData` and `WebView.loadDataWithBaseUrl` methods and implements the `loadFile` and `loadHtmlString` methods from the platform interface. +* Updates to webview_flutter_platform_interface version 1.5.2. + +## 2.3.1 + +* Adds explanation on how to generate the pigeon communication layer and mockito mock objects. +* Updates compileSdkVersion to 31. + +## 2.3.0 + +* Replaces platform implementation with API built with pigeon. + +## 2.2.1 + +* Fix `NullPointerException` from a race condition when changing focus. This only affects `WebView` +when it is created without Hybrid Composition. + +## 2.2.0 + +* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. + +## 2.1.0 + +* Add `zoomEnabled` functionality. + +## 2.0.15 + +* Added Overrides in FlutterWebView.java + +## 2.0.14 + +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). + ## 2.0.13 * Extract Android implementation from `webview_flutter`. - diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index 38838562d13c..80f1f6e0b9cb 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -7,6 +7,39 @@ The Android implementation of [`webview_flutter`][1]. This package is [endorsed][2], which means you can simply use `webview_flutter` normally. This package will be automatically included in your app when you do. +## 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 +`flutter pub run pigeon --input pigeons/android_webview.dart`. + +Due to [flutter/flutter#97744](https://github.com/flutter/flutter/issues/97744), the generated test +pigeon file needs one of its imports updated to properly work with `mockito`. + +In `test/android_webview.pigeon.dart`, change + +```dart +import '../lib/src/android_webview.pigeon.dart'; +``` + +to + +```dart +import 'package:webview_flutter_android/src/android_webview.pigeon.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: +```bash +flutter packages pub run build_runner build --delete-conflicting-outputs +``` + +If you would like to contribute to the plugin, check out our [contribution guide][5]. + [1]: https://pub.dev/packages/webview_flutter [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/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 4a164317c60f..6ba3f65dbedc 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:3.3.0' + classpath 'com.android.tools.build:gradle:7.2.1' } } @@ -22,7 +22,7 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { minSdkVersion 19 @@ -35,13 +35,17 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.annotation:annotation:1.4.0' implementation 'androidx.webkit:webkit:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-inline:3.11.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-inline:4.6.1' testImplementation 'androidx.test:core:1.3.0' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java new file mode 100644 index 000000000000..3e38ce94b3a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.webkit.CookieManager; + +class CookieManagerHostApiImpl implements GeneratedAndroidWebView.CookieManagerHostApi { + @Override + public void clearCookies(GeneratedAndroidWebView.Result result) { + CookieManager cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(result::success); + } else { + final boolean hasCookies = cookieManager.hasCookies(); + if (hasCookies) { + cookieManager.removeAllCookie(); + } + result.success(hasCookies); + } + } + + @Override + public void setCookie(String url, String value) { + CookieManager.getInstance().setCookie(url, value); + } +} 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 new file mode 100644 index 000000000000..2dd98c47d582 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi; + +/** + * Flutter Api implementation for {@link DownloadListener}. + * + *

Passes arguments of callbacks methods from a {@link DownloadListener} to Dart. + */ +public class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public DownloadListenerFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link DownloadListener#onDownloadStart} to Dart. */ + public void onDownloadStart( + DownloadListener downloadListener, + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength, + Reply callback) { + onDownloadStart( + instanceManager.getInstanceId(downloadListener), + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + callback); + } + + /** + * Communicates to Dart that the reference to a {@link DownloadListener} was removed. + * + * @param downloadListener the instance whose reference will be removed + * @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); + } else { + callback.reply(null); + } + } +} 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 new file mode 100644 index 000000000000..9694f396ad2e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; + +/** + * Host api implementation for {@link DownloadListener}. + * + *

Handles creating {@link DownloadListener}s that intercommunicate with a paired Dart object. + */ +public class DownloadListenerHostApiImpl implements DownloadListenerHostApi { + private final InstanceManager instanceManager; + private final DownloadListenerCreator downloadListenerCreator; + private final DownloadListenerFlutterApiImpl flutterApi; + + /** + * Implementation of {@link DownloadListener} that passes arguments of callback methods to Dart. + * + *

No messages are sent to Dart after {@link DownloadListenerImpl#release} is called. + */ + public static class DownloadListenerImpl implements DownloadListener, Releasable { + @Nullable private DownloadListenerFlutterApiImpl flutterApi; + + /** + * Creates a {@link DownloadListenerImpl} that passes arguments of callbacks methods to Dart. + * + * @param flutterApi handles sending messages to Dart + */ + public DownloadListenerImpl(@NonNull DownloadListenerFlutterApiImpl flutterApi) { + this.flutterApi = flutterApi; + } + + @Override + public void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength) { + if (flutterApi != null) { + flutterApi.onDownloadStart( + this, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); + } + } + + @Override + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } + } + + /** Handles creating {@link DownloadListenerImpl}s for a {@link DownloadListenerHostApiImpl}. */ + public static class DownloadListenerCreator { + /** + * Creates a {@link DownloadListenerImpl}. + * + * @param flutterApi handles sending messages to Dart + * @return the created {@link DownloadListenerImpl} + */ + public DownloadListenerImpl createDownloadListener(DownloadListenerFlutterApiImpl flutterApi) { + return new DownloadListenerImpl(flutterApi); + } + } + + /** + * Creates a host API that handles creating {@link DownloadListener}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param downloadListenerCreator handles creating {@link DownloadListenerImpl}s + * @param flutterApi handles sending messages to Dart + */ + public DownloadListenerHostApiImpl( + InstanceManager instanceManager, + DownloadListenerCreator downloadListenerCreator, + DownloadListenerFlutterApiImpl flutterApi) { + this.instanceManager = instanceManager; + this.downloadListenerCreator = downloadListenerCreator; + this.flutterApi = flutterApi; + } + + @Override + public void create(Long instanceId) { + final DownloadListener downloadListener = + downloadListenerCreator.createDownloadListener(flutterApi); + instanceManager.addInstance(downloadListener, instanceId); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java new file mode 100644 index 000000000000..1d484d8639a0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManager.java @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter 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.content.res.AssetManager; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.PluginRegistry; +import java.io.IOException; + +/** Provides access to the assets registered as part of the App bundle. */ +abstract class FlutterAssetManager { + final AssetManager assetManager; + + /** + * Constructs a new instance of the {@link FlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within the + * App bundle. + */ + public FlutterAssetManager(AssetManager assetManager) { + this.assetManager = assetManager; + } + + /** + * Gets the relative file path to the Flutter asset with the given name, including the file's + * extension, e.g., "myImage.jpg". + * + *

The returned file path is relative to the Android app's standard asset's directory. + * Therefore, the returned path is appropriate to pass to Android's AssetManager, but the path is + * not appropriate to load as an absolute path. + */ + abstract String getAssetFilePathByName(String name); + + /** + * Returns a String array of all the assets at the given path. + * + * @param path A relative path within the assets, i.e., "docs/home.html". This value cannot be + * null. + * @return String[] Array of strings, one for each asset. These file names are relative to 'path'. + * This value may be null. + * @throws IOException Throws an IOException in case I/O operations were interrupted. + */ + public String[] list(@NonNull String path) throws IOException { + return assetManager.list(path); + } + + /** + * Provides access to assets using the {@link PluginRegistry.Registrar} for looking up file paths + * to Flutter assets. + * + * @deprecated The {@link RegistrarFlutterAssetManager} is for Flutter's v1 embedding. For + * instructions on migrating a plugin from Flutter's v1 Android embedding to v2, visit + * http://flutter.dev/go/android-plugin-migration + */ + @Deprecated + static class RegistrarFlutterAssetManager extends FlutterAssetManager { + final PluginRegistry.Registrar registrar; + + /** + * Constructs a new instance of the {@link RegistrarFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param registrar Instance of {@link io.flutter.plugin.common.PluginRegistry.Registrar} used + * to look up file paths to assets registered by Flutter. + */ + RegistrarFlutterAssetManager(AssetManager assetManager, PluginRegistry.Registrar registrar) { + super(assetManager); + this.registrar = registrar; + } + + @Override + public String getAssetFilePathByName(String name) { + return registrar.lookupKeyForAsset(name); + } + } + + /** + * Provides access to assets using the {@link FlutterPlugin.FlutterAssets} for looking up file + * paths to Flutter assets. + */ + static class PluginBindingFlutterAssetManager extends FlutterAssetManager { + final FlutterPlugin.FlutterAssets flutterAssets; + + /** + * Constructs a new instance of the {@link PluginBindingFlutterAssetManager}. + * + * @param assetManager Instance of Android's {@link AssetManager} used to access assets within + * the App bundle. + * @param flutterAssets Instance of {@link + * io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets} used to look up file + * paths to assets registered by Flutter. + */ + PluginBindingFlutterAssetManager( + AssetManager assetManager, FlutterPlugin.FlutterAssets flutterAssets) { + super(assetManager); + this.flutterAssets = flutterAssets; + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssets.getAssetFilePathByName(name); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java new file mode 100644 index 000000000000..791912adb815 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Host api implementation for {@link WebView}. + * + *

Handles creating {@link WebView}s that intercommunicate with a paired Dart object. + */ +public class FlutterAssetManagerHostApiImpl implements FlutterAssetManagerHostApi { + final FlutterAssetManager flutterAssetManager; + + /** Constructs a new instance of {@link FlutterAssetManagerHostApiImpl}. */ + public FlutterAssetManagerHostApiImpl(FlutterAssetManager flutterAssetManager) { + this.flutterAssetManager = flutterAssetManager; + } + + @Override + public List list(String path) { + try { + String[] paths = flutterAssetManager.list(path); + + if (paths == null) { + return new ArrayList<>(); + } + + return Arrays.asList(paths); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + } + + @Override + public String getAssetFilePathByName(String name) { + return flutterAssetManager.getAssetFilePathByName(name); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index df3f21daadeb..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -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; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java deleted file mode 100644 index cfad4e315514..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java +++ /dev/null @@ -1,33 +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.webviewflutter; - -import android.webkit.DownloadListener; -import android.webkit.WebView; - -/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ -public class FlutterDownloadListener implements DownloadListener { - private final FlutterWebViewClient webViewClient; - private WebView webView; - - public FlutterDownloadListener(FlutterWebViewClient webViewClient) { - this.webViewClient = webViewClient; - } - - /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ - public void setWebView(WebView webView) { - this.webView = webView; - } - - @Override - public void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - long contentLength) { - webViewClient.notifyDownload(webView, url); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index 4651a5f5ae22..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,498 +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.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -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.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class FlutterWebView implements PlatformView, MethodCallHandler { - - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final WebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - flutterWebViewClient.onLoadingProgress(progress); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - MethodChannel methodChannel, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - - this.methodChannel = methodChannel; - this.methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - - FlutterDownloadListener flutterDownloadListener = - new FlutterDownloadListener(flutterWebViewClient); - webView = - createWebView( - new WebViewBuilder(context, containerView), - params, - new FlutterWebChromeClient(), - flutterDownloadListener); - flutterDownloadListener.setWebView(webView); - - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - - Map settings = (Map) params.get("settings"); - if (settings != null) { - applySettings(settings); - } - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) { - registerJavaScriptChannelNames(names); - } - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) { - updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - } - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - /** - * Creates a {@link android.webkit.WebView} and configures it according to the supplied - * parameters. - * - *

The {@link WebView} is configured with the following predefined settings: - * - *

    - *
  • always enable the DOM storage API; - *
  • always allow JavaScript to automatically open windows; - *
  • always allow support for multiple windows; - *
  • always use the {@link FlutterWebChromeClient} as web Chrome client. - *
- * - *

Important: This method is visible for testing purposes only and should - * never be called from outside this class. - * - * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link - * WebView}. - * @param params creation parameters received over the method channel. - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return The new {@link android.webkit.WebView} object. - */ - @VisibleForTesting - static WebView createWebView( - WebViewBuilder webViewBuilder, - Map params, - WebChromeClient webChromeClient, - @Nullable DownloadListener downloadListener) { - boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); - webViewBuilder - .setUsesHybridComposition(usesHybridComposition) - .setDomStorageEnabled(true) // Always enable DOM storage API. - .setJavaScriptCanOpenWindowsAutomatically( - true) // Always allow automatically opening of windows. - .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient(webChromeClient) - .setDownloadListener( - downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. - - return webViewBuilder.build(); - } - - @Override - public View getView() { - return webView; - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionUnlocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).unlockInputConnection(); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionLocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).lockInputConnection(); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewAttached(View flutterView) { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(flutterView); - } - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewDetached() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(null); - } - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) { - updateJsMode(mode); - } - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "hasProgressTracking": - flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "allowsInlineMediaPlayback": - // no-op inline media playback is always allowed on Android. - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).dispose(); - } - webView.destroy(); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index 260ef8e8b15d..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - boolean hasProgressTracking; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - /** - * Notifies the Flutter code that a download should start when a navigation delegate is set. - * - * @param view the webView the result of the navigation delegate will be send to. - * @param url the download url - * @return A boolean whether or not the request is forwarded to the Flutter code. - */ - boolean notifyDownload(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - void onLoadingProgress(int progress) { - if (hasProgressTracking) { - Map args = new HashMap<>(); - args.put("progress", progress); - methodChannel.invokeMethod("onProgress", args); - } - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java index 8fe58104a0fb..9b3cd471bb83 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -5,29 +5,24 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; -public final class FlutterWebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; +class FlutterWebViewFactory extends PlatformViewFactory { + private final InstanceManager instanceManager; - FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { + FlutterWebViewFactory(InstanceManager instanceManager) { super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; + this.instanceManager = instanceManager; } - @SuppressWarnings("unchecked") @Override public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - return new FlutterWebView(context, methodChannel, params, containerView); + final PlatformView view = (PlatformView) instanceManager.getInstance((Integer) args); + if (view == null) { + throw new IllegalStateException("Unable to find WebView instance: " + args); + } + return view; } } 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 new file mode 100644 index 000000000000..2e163311d6d4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -0,0 +1,2456 @@ +// Copyright 2013 The Flutter 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 + +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; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** 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 @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 @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 @Nullable Boolean isRedirect; + + public @Nullable Boolean getIsRedirect() { + return isRedirect; + } + + public void setIsRedirect(@Nullable Boolean setterArg) { + this.isRedirect = 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 @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 @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; + } + + /** 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); + toMapResult.put("isRedirect", isRedirect); + toMapResult.put("hasGesture", hasGesture); + toMapResult.put("method", method); + toMapResult.put("requestHeaders", requestHeaders); + return toMapResult; + } + + static @NonNull WebResourceRequestData fromMap(@NonNull Map map) { + WebResourceRequestData pigeonResult = new WebResourceRequestData(); + Object url = map.get("url"); + pigeonResult.setUrl((String) url); + Object isForMainFrame = map.get("isForMainFrame"); + pigeonResult.setIsForMainFrame((Boolean) isForMainFrame); + Object isRedirect = map.get("isRedirect"); + pigeonResult.setIsRedirect((Boolean) isRedirect); + Object hasGesture = map.get("hasGesture"); + pigeonResult.setHasGesture((Boolean) hasGesture); + Object method = map.get("method"); + pigeonResult.setMethod((String) method); + Object requestHeaders = map.get("requestHeaders"); + pigeonResult.setRequestHeaders((Map) requestHeaders); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class WebResourceErrorData { + 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 @NonNull String description; + + public @NonNull String getDescription() { + return description; + } + + public void setDescription(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"description\" is null."); + } + this.description = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private 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 @NonNull WebResourceErrorData fromMap(@NonNull Map map) { + WebResourceErrorData pigeonResult = new WebResourceErrorData(); + Object errorCode = map.get("errorCode"); + pigeonResult.setErrorCode( + (errorCode == null) + ? null + : ((errorCode instanceof Integer) ? (Integer) errorCode : (Long) errorCode)); + Object description = map.get("description"); + pigeonResult.setDescription((String) description); + return pigeonResult; + } + } + + 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() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CookieManagerHostApi { + void clearCookies(Result result); + + 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`. + */ + static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.CookieManagerHostApi.clearCookies", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + Result resultCallback = + new Result() { + public void success(Boolean result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.clearCookies(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.CookieManagerHostApi.setCookie", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String urlArg = (String) args.get(0); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + String valueArg = (String) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setCookie(urlArg, valueArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class WebViewHostApiCodec extends StandardMessageCodec { + public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec(); + + private WebViewHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface WebViewHostApi { + 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); + + 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`. */ + static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean useHybridCompositionArg = (Boolean) args.get(1); + if (useHybridCompositionArg == null) { + throw new NullPointerException("useHybridCompositionArg unexpectedly null."); + } + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + useHybridCompositionArg); + 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.WebViewHostApi.dispose", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.dispose((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.WebViewHostApi.loadData", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String dataArg = (String) args.get(1); + if (dataArg == null) { + throw new NullPointerException("dataArg unexpectedly null."); + } + String mimeTypeArg = (String) args.get(2); + String encodingArg = (String) args.get(3); + api.loadData( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + dataArg, + mimeTypeArg, + encodingArg); + 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.WebViewHostApi.loadDataWithBaseUrl", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String baseUrlArg = (String) args.get(1); + String dataArg = (String) args.get(2); + if (dataArg == null) { + throw new NullPointerException("dataArg unexpectedly null."); + } + String mimeTypeArg = (String) args.get(3); + String encodingArg = (String) args.get(4); + String historyUrlArg = (String) args.get(5); + api.loadDataWithBaseUrl( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + baseUrlArg, + dataArg, + mimeTypeArg, + encodingArg, + historyUrlArg); + 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.WebViewHostApi.loadUrl", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String urlArg = (String) args.get(1); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + Map headersArg = (Map) args.get(2); + if (headersArg == null) { + throw new NullPointerException("headersArg unexpectedly null."); + } + api.loadUrl( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + urlArg, + headersArg); + 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.WebViewHostApi.postUrl", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String urlArg = (String) args.get(1); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + byte[] dataArg = (byte[]) args.get(2); + if (dataArg == null) { + throw new NullPointerException("dataArg unexpectedly null."); + } + api.postUrl( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, dataArg); + 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.WebViewHostApi.getUrl", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String output = + api.getUrl((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.canGoBack", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean output = + api.canGoBack((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.canGoForward", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean output = + api.canGoForward((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.goBack", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.goBack((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.WebViewHostApi.goForward", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.goForward((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.WebViewHostApi.reload", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.reload((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.WebViewHostApi.clearCache", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean includeDiskFilesArg = (Boolean) args.get(1); + if (includeDiskFilesArg == null) { + throw new NullPointerException("includeDiskFilesArg unexpectedly null."); + } + api.clearCache( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + includeDiskFilesArg); + 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.WebViewHostApi.evaluateJavascript", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String javascriptStringArg = (String) args.get(1); + if (javascriptStringArg == null) { + throw new NullPointerException("javascriptStringArg unexpectedly null."); + } + Result resultCallback = + new Result() { + public void success(String result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.evaluateJavascript( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + javascriptStringArg, + 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.WebViewHostApi.getTitle", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String output = + api.getTitle((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.scrollTo", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number xArg = (Number) args.get(1); + if (xArg == null) { + throw new NullPointerException("xArg unexpectedly null."); + } + Number yArg = (Number) args.get(2); + if (yArg == null) { + throw new NullPointerException("yArg unexpectedly null."); + } + 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) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.scrollBy", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number xArg = (Number) args.get(1); + if (xArg == null) { + throw new NullPointerException("xArg unexpectedly null."); + } + Number yArg = (Number) args.get(2); + if (yArg == null) { + throw new NullPointerException("yArg unexpectedly null."); + } + 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) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.getScrollX", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Long output = + api.getScrollX((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.getScrollY", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + 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.setWebContentsDebuggingEnabled", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Boolean enabledArg = (Boolean) args.get(0); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setWebContentsDebuggingEnabled(enabledArg); + 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.WebViewHostApi.setWebViewClient", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number webViewClientInstanceIdArg = (Number) args.get(1); + if (webViewClientInstanceIdArg == null) { + throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); + } + api.setWebViewClient( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (webViewClientInstanceIdArg == null) + ? null + : webViewClientInstanceIdArg.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.WebViewHostApi.addJavaScriptChannel", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number javaScriptChannelInstanceIdArg = (Number) args.get(1); + if (javaScriptChannelInstanceIdArg == null) { + throw new NullPointerException( + "javaScriptChannelInstanceIdArg unexpectedly null."); + } + api.addJavaScriptChannel( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (javaScriptChannelInstanceIdArg == null) + ? null + : javaScriptChannelInstanceIdArg.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.WebViewHostApi.removeJavaScriptChannel", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number javaScriptChannelInstanceIdArg = (Number) args.get(1); + if (javaScriptChannelInstanceIdArg == null) { + throw new NullPointerException( + "javaScriptChannelInstanceIdArg unexpectedly null."); + } + api.removeJavaScriptChannel( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (javaScriptChannelInstanceIdArg == null) + ? null + : javaScriptChannelInstanceIdArg.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.WebViewHostApi.setDownloadListener", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number listenerInstanceIdArg = (Number) args.get(1); + api.setDownloadListener( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (listenerInstanceIdArg == null) ? null : listenerInstanceIdArg.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.WebViewHostApi.setWebChromeClient", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number clientInstanceIdArg = (Number) args.get(1); + api.setWebChromeClient( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (clientInstanceIdArg == null) ? null : clientInstanceIdArg.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.WebViewHostApi.setBackgroundColor", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number colorArg = (Number) args.get(1); + if (colorArg == null) { + throw new NullPointerException("colorArg unexpectedly null."); + } + api.setBackgroundColor( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (colorArg == null) ? null : colorArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + 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. */ + public interface WebSettingsHostApi { + 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); + + /** The codec used by WebSettingsHostApi. */ + static MessageCodec getCodec() { + return WebSettingsHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `WebSettingsHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebSettingsHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number webViewInstanceIdArg = (Number) args.get(1); + if (webViewInstanceIdArg == null) { + throw new NullPointerException("webViewInstanceIdArg unexpectedly null."); + } + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (webViewInstanceIdArg == null) ? null : webViewInstanceIdArg.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.WebSettingsHostApi.dispose", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + api.dispose((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.WebSettingsHostApi.setDomStorageEnabled", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean flagArg = (Boolean) args.get(1); + if (flagArg == null) { + throw new NullPointerException("flagArg unexpectedly null."); + } + api.setDomStorageEnabled( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); + 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.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean flagArg = (Boolean) args.get(1); + if (flagArg == null) { + throw new NullPointerException("flagArg unexpectedly null."); + } + api.setJavaScriptCanOpenWindowsAutomatically( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); + 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.WebSettingsHostApi.setSupportMultipleWindows", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean supportArg = (Boolean) args.get(1); + if (supportArg == null) { + throw new NullPointerException("supportArg unexpectedly null."); + } + api.setSupportMultipleWindows( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); + 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.WebSettingsHostApi.setJavaScriptEnabled", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean flagArg = (Boolean) args.get(1); + if (flagArg == null) { + throw new NullPointerException("flagArg unexpectedly null."); + } + api.setJavaScriptEnabled( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); + 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.WebSettingsHostApi.setUserAgentString", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String userAgentStringArg = (String) args.get(1); + api.setUserAgentString( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + userAgentStringArg); + 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.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean requireArg = (Boolean) args.get(1); + if (requireArg == null) { + throw new NullPointerException("requireArg unexpectedly null."); + } + api.setMediaPlaybackRequiresUserGesture( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), requireArg); + 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.WebSettingsHostApi.setSupportZoom", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean supportArg = (Boolean) args.get(1); + if (supportArg == null) { + throw new NullPointerException("supportArg unexpectedly null."); + } + api.setSupportZoom( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); + 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.WebSettingsHostApi.setLoadWithOverviewMode", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean overviewArg = (Boolean) args.get(1); + if (overviewArg == null) { + throw new NullPointerException("overviewArg unexpectedly null."); + } + api.setLoadWithOverviewMode( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), overviewArg); + 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.WebSettingsHostApi.setUseWideViewPort", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean useArg = (Boolean) args.get(1); + if (useArg == null) { + throw new NullPointerException("useArg unexpectedly null."); + } + api.setUseWideViewPort( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), useArg); + 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.WebSettingsHostApi.setDisplayZoomControls", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setDisplayZoomControls( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); + 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.WebSettingsHostApi.setBuiltInZoomControls", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setBuiltInZoomControls( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); + 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.WebSettingsHostApi.setAllowFileAccess", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setAllowFileAccess( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + 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. */ + public interface JavaScriptChannelHostApi { + 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`. + */ + static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + String channelNameArg = (String) args.get(1); + if (channelNameArg == null) { + throw new NullPointerException("channelNameArg unexpectedly null."); + } + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), channelNameArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + 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. */ + public static class JavaScriptChannelFlutterApi { + private final BinaryMessenger binaryMessenger; + + public JavaScriptChannelFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return JavaScriptChannelFlutterApiCodec.INSTANCE; + } + + 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 -> { + callback.reply(null); + }); + } + + 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 -> { + callback.reply(null); + }); + } + } + + 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. */ + public interface WebViewClientHostApi { + 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`. + */ + static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewClientHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean shouldOverrideUrlLoadingArg = (Boolean) args.get(1); + if (shouldOverrideUrlLoadingArg == null) { + throw new NullPointerException( + "shouldOverrideUrlLoadingArg unexpectedly null."); + } + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + shouldOverrideUrlLoadingArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class WebViewClientFlutterApiCodec extends StandardMessageCodec { + public static final WebViewClientFlutterApiCodec INSTANCE = new WebViewClientFlutterApiCodec(); + + private WebViewClientFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return WebResourceErrorData.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return WebResourceRequestData.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof WebResourceErrorData) { + stream.write(128); + writeValue(stream, ((WebResourceErrorData) value).toMap()); + } else if (value instanceof WebResourceRequestData) { + stream.write(129); + writeValue(stream, ((WebResourceRequestData) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** 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) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return WebViewClientFlutterApiCodec.INSTANCE; + } + + 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 -> { + callback.reply(null); + }); + } + + 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( + @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( + @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( + @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( + @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( + @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 -> { + callback.reply(null); + }); + } + } + + 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. */ + public interface DownloadListenerHostApi { + 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`. + */ + static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.DownloadListenerHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + 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); + } + } + } + } + + 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. */ + public static class DownloadListenerFlutterApi { + private final BinaryMessenger binaryMessenger; + + public DownloadListenerFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return DownloadListenerFlutterApiCodec.INSTANCE; + } + + 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 -> { + callback.reply(null); + }); + } + + 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 -> { + callback.reply(null); + }); + } + } + + 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. */ + public interface WebChromeClientHostApi { + 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`. + */ + static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebChromeClientHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Number webViewClientInstanceIdArg = (Number) args.get(1); + if (webViewClientInstanceIdArg == null) { + throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); + } + api.create( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + (webViewClientInstanceIdArg == null) + ? null + : webViewClientInstanceIdArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + 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. */ + public interface FlutterAssetManagerHostApi { + @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`. + */ + static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FlutterAssetManagerHostApi.list", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String pathArg = (String) args.get(0); + if (pathArg == null) { + throw new NullPointerException("pathArg unexpectedly null."); + } + List output = api.list(pathArg); + 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.FlutterAssetManagerHostApi.getAssetFilePathByName", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String nameArg = (String) args.get(0); + if (nameArg == null) { + throw new NullPointerException("nameArg unexpectedly null."); + } + String output = api.getAssetFilePathByName(nameArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + 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. */ + public static class WebChromeClientFlutterApi { + private final BinaryMessenger binaryMessenger; + + public WebChromeClientFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return WebChromeClientFlutterApiCodec.INSTANCE; + } + + 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 -> { + callback.reply(null); + }); + } + + 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 -> { + callback.reply(null); + }); + } + } + + private static class WebStorageHostApiCodec extends StandardMessageCodec { + public static final WebStorageHostApiCodec INSTANCE = new WebStorageHostApiCodec(); + + private WebStorageHostApiCodec() {} + } + + /** 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 WebStorageHostApiCodec.INSTANCE; + } + + /** + * 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; + 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; + 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); + } + } + } + } + + 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/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java index 51b2a3809fff..1276ac81acae 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -25,7 +25,7 @@ * *

See also {@link ThreadedInputConnectionProxyAdapterView}. */ -final class InputAwareWebView extends WebView { +class InputAwareWebView extends WebView { private static final String TAG = "InputAwareWebView"; private View threadedInputConnectionProxyView; private ThreadedInputConnectionProxyAdapterView proxyAdapterView; @@ -158,7 +158,7 @@ private void resetInputConnection() { *

{@code targetView} should have a {@link View#getHandler} method with the thread that future * InputConnections should be created on. */ - private void setInputConnectionTarget(final View targetView) { + void setInputConnectionTarget(final View targetView) { if (containerView == null) { Log.e( TAG, @@ -171,6 +171,13 @@ private void setInputConnectionTarget(final View targetView) { new Runnable() { @Override public void run() { + if (containerView == null) { + Log.e( + TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); // This is a hack to make InputMethodManager believe that the target view now has focus. 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 new file mode 100644 index 000000000000..a368baf266dd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.util.LongSparseArray; +import java.util.HashMap; +import java.util.Map; + +/** + * Maintains instances to intercommunicate with Dart objects. + * + *

When an instance is added with an instanceId, either can be used to retrieve the other. + */ +public class InstanceManager { + private final LongSparseArray instanceIdsToInstances = new LongSparseArray<>(); + private final Map instancesToInstanceIds = new HashMap<>(); + + /** + * Add a new instance to the manager. + * + *

If an instance or instanceId has already been added, it will be replaced by the new values. + * + * @param instance the new object to be added + * @param instanceId unique id of the added object + */ + public void addInstance(Object instance, long instanceId) { + instancesToInstanceIds.put(instance, instanceId); + instanceIdsToInstances.append(instanceId, instance); + } + + /** + * Remove the instance with instanceId from the manager. + * + * @param instanceId the id of the instance to be removed + * @return the removed instance if the manager contains the instanceId, otherwise null + */ + public Object removeInstanceWithId(long instanceId) { + final Object instance = instanceIdsToInstances.get(instanceId); + if (instance != null) { + instanceIdsToInstances.remove(instanceId); + instancesToInstanceIds.remove(instance); + } + return instance; + } + + /** + * Remove the instance from the manager. + * + * @param instance the instance to be removed + * @return the instanceId of the removed instance if the manager contains the value, otherwise + * null + */ + public Long removeInstance(Object instance) { + final Long instanceId = instancesToInstanceIds.get(instance); + if (instanceId != null) { + instanceIdsToInstances.remove(instanceId); + instancesToInstanceIds.remove(instance); + } + return instanceId; + } + + /** + * Retrieve the Object paired with instanceId. + * + * @param instanceId the instanceId of the desired instance + * @return the instance stored with the instanceId if the manager contains the value, otherwise + * null + */ + public Object getInstance(long instanceId) { + return instanceIdsToInstances.get(instanceId); + } + + /** + * Retrieve the instanceId paired with an instance. + * + * @param instance the value paired with the desired instanceId + * @return the instanceId paired with instance if the manager contains the value, otherwise null + */ + public Long getInstanceId(Object instance) { + return instancesToInstanceIds.get(instance); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index 4d596351b3d0..ce6f2b81ed1c 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -7,31 +7,36 @@ import android.os.Handler; import android.os.Looper; import android.webkit.JavascriptInterface; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; /** * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets * up. * - *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method - * channel to the Dart code. + *

Exposes a single method named `postMessage` to JavaScript, which sends a message to the Dart + * code. + * + *

No messages are sent to Dart after {@link JavaScriptChannel#release} is called. */ -class JavaScriptChannel { - private final MethodChannel methodChannel; - private final String javaScriptChannelName; +public class JavaScriptChannel implements Releasable { private final Handler platformThreadHandler; + final String javaScriptChannelName; + @Nullable private JavaScriptChannelFlutterApiImpl flutterApi; /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through + * Creates a {@link JavaScriptChannel} that passes arguments of callback methods to Dart. + * + * @param flutterApi the Flutter Api to which JS messages are sent + * @param channelName JavaScript channel the message was sent through + * @param platformThreadHandler handles making callbacks on the desired thread */ - JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; + public JavaScriptChannel( + @NonNull JavaScriptChannelFlutterApiImpl flutterApi, + String channelName, + Handler platformThreadHandler) { + this.flutterApi = flutterApi; + this.javaScriptChannelName = channelName; this.platformThreadHandler = platformThreadHandler; } @@ -39,20 +44,25 @@ class JavaScriptChannel { @SuppressWarnings("unused") @JavascriptInterface public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); + final Runnable postMessageRunnable = + () -> { + if (flutterApi != null) { + flutterApi.postMessage(JavaScriptChannel.this, message, reply -> {}); } }; + if (platformThreadHandler.getLooper() == Looper.myLooper()) { postMessageRunnable.run(); } else { platformThreadHandler.post(postMessageRunnable); } } + + @Override + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } } 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 new file mode 100644 index 000000000000..120f66dbdf9a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; + +/** + * Flutter Api implementation for {@link JavaScriptChannel}. + * + *

Passes arguments of callbacks methods from a {@link JavaScriptChannel} to Dart. + */ +public class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger Handles sending messages to Dart. + * @param instanceManager Maintains instances stored to communicate with Dart objects. + */ + public JavaScriptChannelFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link JavaScriptChannel#postMessage} to Dart. */ + public void postMessage( + JavaScriptChannel javaScriptChannel, String messageArg, Reply callback) { + super.postMessage(instanceManager.getInstanceId(javaScriptChannel), messageArg, callback); + } + + /** + * Communicates to Dart that the reference to a {@link JavaScriptChannel} was removed. + * + * @param javaScriptChannel The instance whose reference will be removed. + * @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); + } else { + callback.reply(null); + } + } +} 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 new file mode 100644 index 000000000000..3055c9fc7f40 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.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.webviewflutter; + +import android.os.Handler; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; + +/** + * Host api implementation for {@link JavaScriptChannel}. + * + *

Handles creating {@link JavaScriptChannel}s that intercommunicate with a paired Dart object. + */ +public class JavaScriptChannelHostApiImpl implements JavaScriptChannelHostApi { + private final InstanceManager instanceManager; + private final JavaScriptChannelCreator javaScriptChannelCreator; + private final JavaScriptChannelFlutterApiImpl flutterApi; + + private Handler platformThreadHandler; + + /** Handles creating {@link JavaScriptChannel}s for a {@link JavaScriptChannelHostApiImpl}. */ + public static class JavaScriptChannelCreator { + /** + * Creates a {@link JavaScriptChannel}. + * + * @param flutterApi handles sending messages to Dart + * @param channelName JavaScript channel the message should be sent through + * @param platformThreadHandler handles making callbacks on the desired thread + * @return the created {@link JavaScriptChannel} + */ + public JavaScriptChannel createJavaScriptChannel( + JavaScriptChannelFlutterApiImpl flutterApi, + String channelName, + Handler platformThreadHandler) { + return new JavaScriptChannel(flutterApi, channelName, platformThreadHandler); + } + } + + /** + * Creates a host API that handles creating {@link JavaScriptChannel}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param javaScriptChannelCreator handles creating {@link JavaScriptChannel}s + * @param flutterApi handles sending messages to Dart + * @param platformThreadHandler handles making callbacks on the desired thread + */ + public JavaScriptChannelHostApiImpl( + InstanceManager instanceManager, + JavaScriptChannelCreator javaScriptChannelCreator, + JavaScriptChannelFlutterApiImpl flutterApi, + Handler platformThreadHandler) { + this.instanceManager = instanceManager; + this.javaScriptChannelCreator = javaScriptChannelCreator; + this.flutterApi = flutterApi; + this.platformThreadHandler = platformThreadHandler; + } + + /** + * Sets the platformThreadHandler to make callbacks + * + * @param platformThreadHandler the new thread handler + */ + public void setPlatformThreadHandler(Handler platformThreadHandler) { + this.platformThreadHandler = platformThreadHandler; + } + + @Override + public void create(Long instanceId, String channelName) { + final JavaScriptChannel javaScriptChannel = + javaScriptChannelCreator.createJavaScriptChannel( + flutterApi, channelName, platformThreadHandler); + instanceManager.addInstance(javaScriptChannel, instanceId); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java new file mode 100644 index 000000000000..9c4ed7650640 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.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.webviewflutter; + +/** + * Represents a resource, or a holder of resources, which may be released once they are no longer + * needed. + */ +interface Releasable { + /** Notify that that the reference to an object will be removed by a holder. */ + void release(); +} 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 new file mode 100644 index 000000000000..2ab9275b41c3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; + +/** + * Flutter Api implementation for {@link WebChromeClient}. + * + *

Passes arguments of callbacks methods from a {@link WebChromeClient} to Dart. + */ +public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public WebChromeClientFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** Passes arguments from {@link WebChromeClient#onProgressChanged} to Dart. */ + public void onProgressChanged( + WebChromeClient webChromeClient, WebView webView, Long progress, Reply callback) { + super.onProgressChanged( + instanceManager.getInstanceId(webChromeClient), + instanceManager.getInstanceId(webView), + progress, + callback); + } + + /** + * Communicates to Dart that the reference to a {@link WebChromeClient}} was removed. + * + * @param webChromeClient the instance whose reference will be removed + * @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); + } else { + callback.reply(null); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java new file mode 100644 index 000000000000..d2e1e59ce179 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.os.Message; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; + +/** + * Host api implementation for {@link WebChromeClient}. + * + *

Handles creating {@link WebChromeClient}s that intercommunicate with a paired Dart object. + */ +public class WebChromeClientHostApiImpl implements WebChromeClientHostApi { + private final InstanceManager instanceManager; + private final WebChromeClientCreator webChromeClientCreator; + private final WebChromeClientFlutterApiImpl flutterApi; + + /** + * Implementation of {@link WebChromeClient} that passes arguments of callback methods to Dart. + */ + public static class WebChromeClientImpl extends WebChromeClient implements Releasable { + @Nullable private WebChromeClientFlutterApiImpl flutterApi; + private WebViewClient webViewClient; + + /** + * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart. + * + * @param flutterApi handles sending messages to Dart + * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow} + */ + public WebChromeClientImpl( + @NonNull WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + this.flutterApi = flutterApi; + this.webViewClient = webViewClient; + } + + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + return onCreateWindow(view, resultMsg, new WebView(view.getContext())); + } + + /** + * Verifies that a url opened by `Window.open` has a secure url. + * + * @param view the WebView from which the request for a new window originated. + * @param resultMsg the message to send when once a new WebView has been created. resultMsg.obj + * is a {@link WebView.WebViewTransport} object. This should be used to transport the new + * WebView, by calling WebView.WebViewTransport.setWebView(WebView) + * @param onCreateWindowWebView the temporary WebView used to verify the url is secure + * @return this method should return true if the host application will create a new window, in + * which case resultMsg should be sent to its target. Otherwise, this method should return + * false. Returning false from this method but also sending resultMsg will result in + * undefined behavior + */ + @VisibleForTesting + boolean onCreateWindow( + final WebView view, Message resultMsg, @Nullable WebView onCreateWindowWebView) { + final WebViewClient windowWebViewClient = + new WebViewClient() { + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView windowWebView, @NonNull WebResourceRequest request) { + if (!webViewClient.shouldOverrideUrlLoading(view, request)) { + view.loadUrl(request.getUrl().toString()); + } + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView windowWebView, String url) { + if (!webViewClient.shouldOverrideUrlLoading(view, url)) { + view.loadUrl(url); + } + return true; + } + }; + + if (onCreateWindowWebView == null) { + onCreateWindowWebView = new WebView(view.getContext()); + } + onCreateWindowWebView.setWebViewClient(windowWebViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(onCreateWindowWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + if (flutterApi != null) { + flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); + } + } + + /** + * Set the {@link WebViewClient} that calls to {@link WebChromeClient#onCreateWindow} are passed + * to. + * + * @param webViewClient the forwarding {@link WebViewClient} + */ + public void setWebViewClient(WebViewClient webViewClient) { + this.webViewClient = webViewClient; + } + + @Override + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } + } + + /** Handles creating {@link WebChromeClient}s for a {@link WebChromeClientHostApiImpl}. */ + public static class WebChromeClientCreator { + /** + * Creates a {@link DownloadListenerHostApiImpl.DownloadListenerImpl}. + * + * @param flutterApi handles sending messages to Dart + * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow} + * @return the created {@link DownloadListenerHostApiImpl.DownloadListenerImpl} + */ + public WebChromeClientImpl createWebChromeClient( + WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + return new WebChromeClientImpl(flutterApi, webViewClient); + } + } + + /** + * Creates a host API that handles creating {@link WebChromeClient}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webChromeClientCreator handles creating {@link WebChromeClient}s + * @param flutterApi handles sending messages to Dart + */ + public WebChromeClientHostApiImpl( + InstanceManager instanceManager, + WebChromeClientCreator webChromeClientCreator, + WebChromeClientFlutterApiImpl flutterApi) { + this.instanceManager = instanceManager; + this.webChromeClientCreator = webChromeClientCreator; + this.flutterApi = flutterApi; + } + + @Override + public void create(Long instanceId, Long webViewClientInstanceId) { + final WebViewClient webViewClient = + (WebViewClient) instanceManager.getInstance(webViewClientInstanceId); + final WebChromeClient webChromeClient = + webChromeClientCreator.createWebChromeClient(flutterApi, webViewClient); + instanceManager.addInstance(webChromeClient, instanceId); + } +} 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 new file mode 100644 index 000000000000..b168e206214f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebSettings; +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; + +/** + * Host api implementation for {@link WebSettings}. + * + *

Handles creating {@link WebSettings}s that intercommunicate with a paired Dart object. + */ +public class WebSettingsHostApiImpl implements WebSettingsHostApi { + private final InstanceManager instanceManager; + private final WebSettingsCreator webSettingsCreator; + + /** Handles creating {@link WebSettings} for a {@link WebSettingsHostApiImpl}. */ + public static class WebSettingsCreator { + /** + * Creates a {@link WebSettings}. + * + * @param webView the {@link WebView} which the settings affect + * @return the created {@link WebSettings} + */ + public WebSettings createWebSettings(WebView webView) { + return webView.getSettings(); + } + } + + /** + * Creates a host API that handles creating {@link WebSettings} and invoke its methods. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webSettingsCreator handles creating {@link WebSettings}s + */ + public WebSettingsHostApiImpl( + InstanceManager instanceManager, WebSettingsCreator webSettingsCreator) { + this.instanceManager = instanceManager; + this.webSettingsCreator = webSettingsCreator; + } + + @Override + public void create(Long instanceId, Long webViewInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(webViewInstanceId); + instanceManager.addInstance(webSettingsCreator.createWebSettings(webView), instanceId); + } + + @Override + public void dispose(Long instanceId) { + instanceManager.removeInstanceWithId(instanceId); + } + + @Override + public void setDomStorageEnabled(Long instanceId, Boolean flag) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setDomStorageEnabled(flag); + } + + @Override + public void setJavaScriptCanOpenWindowsAutomatically(Long instanceId, Boolean flag) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setJavaScriptCanOpenWindowsAutomatically(flag); + } + + @Override + public void setSupportMultipleWindows(Long instanceId, Boolean support) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setSupportMultipleWindows(support); + } + + @Override + public void setJavaScriptEnabled(Long instanceId, Boolean flag) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setJavaScriptEnabled(flag); + } + + @Override + public void setUserAgentString(Long instanceId, String userAgentString) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setUserAgentString(userAgentString); + } + + @Override + public void setMediaPlaybackRequiresUserGesture(Long instanceId, Boolean require) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setMediaPlaybackRequiresUserGesture(require); + } + + @Override + public void setSupportZoom(Long instanceId, Boolean support) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setSupportZoom(support); + } + + @Override + public void setLoadWithOverviewMode(Long instanceId, Boolean overview) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setLoadWithOverviewMode(overview); + } + + @Override + public void setUseWideViewPort(Long instanceId, Boolean use) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setUseWideViewPort(use); + } + + @Override + public void setDisplayZoomControls(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setDisplayZoomControls(enabled); + } + + @Override + public void setBuiltInZoomControls(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setBuiltInZoomControls(enabled); + } + + @Override + public void setAllowFileAccess(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setAllowFileAccess(enabled); + } +} 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..42e7603c0279 --- /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.addInstance(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/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java deleted file mode 100644 index d3cd1d57cdae..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ /dev/null @@ -1,155 +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.webviewflutter; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** Builder used to create {@link android.webkit.WebView} objects. */ -public class WebViewBuilder { - - /** Factory used to create a new {@link android.webkit.WebView} instance. */ - static class WebViewFactory { - - /** - * Creates a new {@link android.webkit.WebView} instance. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is - * returned. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set - * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or - * IME, thread (see also {@link InputAwareWebView}) - * @return A new instance of the {@link android.webkit.WebView} object. - */ - static WebView create(Context context, boolean usesHybridComposition, View containerView) { - return usesHybridComposition - ? new WebView(context) - : new InputAwareWebView(context, containerView); - } - } - - private final Context context; - private final View containerView; - - private boolean enableDomStorage; - private boolean javaScriptCanOpenWindowsAutomatically; - private boolean supportMultipleWindows; - private boolean usesHybridComposition; - private WebChromeClient webChromeClient; - private DownloadListener downloadListener; - - /** - * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link - * WebViewFactory} object. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to - * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, - * thread (see also {@link InputAwareWebView}) - */ - WebViewBuilder(@NonNull final Context context, View containerView) { - this.context = context; - this.containerView = containerView; - } - - /** - * Sets whether the DOM storage API is enabled. The default value is {@code false}. - * - * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDomStorageEnabled(boolean flag) { - this.enableDomStorage = flag; - return this; - } - - /** - * Sets whether JavaScript is allowed to open windows automatically. This applies to the - * JavaScript function {@code window.open()}. The default value is {@code false}. - * - * @param flag {@code true} if JavaScript is allowed to open windows automatically. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { - this.javaScriptCanOpenWindowsAutomatically = flag; - return this; - } - - /** - * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link - * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is - * {@code false}. - * - * @param flag {@code true} if multiple windows are supported. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setSupportMultipleWindows(boolean flag) { - this.supportMultipleWindows = flag; - return this; - } - - /** - * Sets whether the hybrid composition should be used. - * - *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the - * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the - * {@link WebView} on Android versions below N. - * - * @param flag {@code true} if uses hybrid composition. The default is {@code false}. - * @return This builder. This value cannot be {@code null} - */ - public WebViewBuilder setUsesHybridComposition(boolean flag) { - this.usesHybridComposition = flag; - return this; - } - - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling - * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. - * - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { - this.webChromeClient = webChromeClient; - return this; - } - - /** - * 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. - * - * @param downloadListener an implementation of DownloadListener This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { - this.downloadListener = downloadListener; - return this; - } - - /** - * Build the {@link android.webkit.WebView} using the current settings. - * - * @return The {@link android.webkit.WebView} using the current settings. - */ - public WebView build() { - WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); - - WebSettings webSettings = webView.getSettings(); - webSettings.setDomStorageEnabled(enableDomStorage); - webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); - webSettings.setSupportMultipleWindows(supportMultipleWindows); - webView.setWebChromeClient(webChromeClient); - webView.setDownloadListener(downloadListener); - return webView; - } -} 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 new file mode 100644 index 000000000000..b4885688f7ac --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.RequiresApi; +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}. + * + *

Passes arguments of callbacks methods from a {@link WebViewClient} to Dart. + */ +public class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { + private final InstanceManager instanceManager; + + @RequiresApi(api = Build.VERSION_CODES.M) + static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( + WebResourceError error) { + return new GeneratedAndroidWebView.WebResourceErrorData.Builder() + .setErrorCode((long) error.getErrorCode()) + .setDescription(error.getDescription().toString()) + .build(); + } + + @SuppressLint("RequiresFeature") + static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( + WebResourceErrorCompat error) { + 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.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()); + } + + return requestData.build(); + } + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public WebViewClientFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + /** 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); + } + + /** 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); + } + + /** + * Passes arguments from {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, + * WebResourceError)} to Dart. + */ + @RequiresApi(api = Build.VERSION_CODES.M) + public void onReceivedRequestError( + WebViewClient webViewClient, + WebView webView, + WebResourceRequest request, + WebResourceError error, + Reply callback) { + onReceivedRequestError( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + callback); + } + + /** + * Passes arguments from {@link androidx.webkit.WebViewClientCompat#onReceivedError(WebView, + * WebResourceRequest, WebResourceError)} to Dart. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void onReceivedRequestError( + WebViewClient webViewClient, + WebView webView, + WebResourceRequest request, + WebResourceErrorCompat error, + Reply callback) { + onReceivedRequestError( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + callback); + } + + /** + * Passes arguments from {@link WebViewClient#onReceivedError(WebView, int, String, String)} to + * Dart. + */ + public void onReceivedError( + WebViewClient webViewClient, + WebView webView, + Long errorCodeArg, + String descriptionArg, + String failingUrlArg, + Reply callback) { + onReceivedError( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + errorCodeArg, + descriptionArg, + failingUrlArg, + callback); + } + + /** + * Passes arguments from {@link WebViewClient#shouldOverrideUrlLoading(WebView, + * WebResourceRequest)} to Dart. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void requestLoading( + WebViewClient webViewClient, + WebView webView, + WebResourceRequest request, + Reply callback) { + requestLoading( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + createWebResourceRequestData(request), + callback); + } + + /** + * Passes arguments from {@link WebViewClient#shouldOverrideUrlLoading(WebView, String)} to Dart. + */ + public void urlLoading( + WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { + urlLoading( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + urlArg, + callback); + } + + /** + * Communicates to Dart that the reference to a {@link WebViewClient} was removed. + * + * @param webViewClient the instance whose reference will be removed + * @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); + } else { + callback.reply(null); + } + } +} 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 new file mode 100644 index 000000000000..6b659fae9c0f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.view.KeyEvent; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebViewClientCompat; + +/** + * Host api implementation for {@link WebViewClient}. + * + *

Handles creating {@link WebViewClient}s that intercommunicate with a paired Dart object. + */ +public class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi { + private final InstanceManager instanceManager; + private final WebViewClientCreator webViewClientCreator; + private final WebViewClientFlutterApiImpl flutterApi; + + /** + * An interface implemented by a class that extends {@link WebViewClient} and {@link Releasable}. + */ + public interface ReleasableWebViewClient extends Releasable {} + + /** Implementation of {@link WebViewClient} that passes arguments of callback methods to Dart. */ + @RequiresApi(Build.VERSION_CODES.N) + public static class WebViewClientImpl extends WebViewClient implements ReleasableWebViewClient { + @Nullable private WebViewClientFlutterApiImpl flutterApi; + private final boolean shouldOverrideUrlLoading; + + /** + * Creates a {@link WebViewClient} that passes arguments of callbacks methods to Dart. + * + * @param flutterApi handles sending messages to Dart + * @param shouldOverrideUrlLoading whether loading a url should be overridden + */ + public WebViewClientImpl( + @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + this.flutterApi = flutterApi; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (flutterApi != null) { + flutterApi.onPageStarted(this, view, url, reply -> {}); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + if (flutterApi != null) { + flutterApi.onPageFinished(this, view, url, reply -> {}); + } + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + if (flutterApi != null) { + flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + if (flutterApi != null) { + flutterApi.onReceivedError( + this, view, (long) errorCode, description, failingUrl, reply -> {}); + } + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (flutterApi != null) { + flutterApi.requestLoading(this, view, request, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (flutterApi != null) { + flutterApi.urlLoading(this, view, url, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } + } + + /** + * Implementation of {@link WebViewClientCompat} that passes arguments of callback methods to + * Dart. + */ + public static class WebViewClientCompatImpl extends WebViewClientCompat + implements ReleasableWebViewClient { + private @Nullable WebViewClientFlutterApiImpl flutterApi; + private final boolean shouldOverrideUrlLoading; + + public WebViewClientCompatImpl( + @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + this.flutterApi = flutterApi; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (flutterApi != null) { + flutterApi.onPageStarted(this, view, url, reply -> {}); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + if (flutterApi != null) { + flutterApi.onPageFinished(this, view, url, reply -> {}); + } + } + + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is + // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint("RequiresFeature") + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (flutterApi != null) { + flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + if (flutterApi != null) { + flutterApi.onReceivedError( + this, view, (long) errorCode, description, failingUrl, reply -> {}); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + if (flutterApi != null) { + flutterApi.requestLoading(this, view, request, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (flutterApi != null) { + flutterApi.urlLoading(this, view, url, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + + public void release() { + if (flutterApi != null) { + flutterApi.dispose(this, reply -> {}); + } + flutterApi = null; + } + } + + /** Handles creating {@link WebViewClient}s for a {@link WebViewClientHostApiImpl}. */ + public static class WebViewClientCreator { + /** + * Creates a {@link WebViewClient}. + * + * @param flutterApi handles sending messages to Dart + * @return the created {@link WebViewClient} + */ + public WebViewClient createWebViewClient( + WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + // WebViewClientCompat is used to get + // shouldOverrideUrlLoading(WebView view, WebResourceRequest request) + // invoked by the webview on older Android devices, without it pages that use iframes will + // be broken when a navigationDelegate is set on Android version earlier than N. + // + // However, this if statement attempts to avoid using WebViewClientCompat on versions >= N due + // to bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see + // https://github.com/flutter/flutter/issues/29446. + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return new WebViewClientImpl(flutterApi, shouldOverrideUrlLoading); + } else { + return new WebViewClientCompatImpl(flutterApi, shouldOverrideUrlLoading); + } + } + } + + /** + * Creates a host API that handles creating {@link WebViewClient}s. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webViewClientCreator handles creating {@link WebViewClient}s + * @param flutterApi handles sending messages to Dart + */ + public WebViewClientHostApiImpl( + InstanceManager instanceManager, + WebViewClientCreator webViewClientCreator, + WebViewClientFlutterApiImpl flutterApi) { + this.instanceManager = instanceManager; + this.webViewClientCreator = webViewClientCreator; + this.flutterApi = flutterApi; + } + + @Override + public void create(Long instanceId, Boolean shouldOverrideUrlLoading) { + final WebViewClient webViewClient = + webViewClientCreator.createWebViewClient(flutterApi, shouldOverrideUrlLoading); + instanceManager.addInstance(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 268d35a1e04c..67202ebef16d 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 @@ -4,20 +4,36 @@ package io.flutter.plugins.webviewflutter; +import android.content.Context; +import android.os.Handler; +import android.view.View; +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; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.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; /** * Java platform implementation of the webview_flutter plugin. * *

Register this in an add to app scenario to gracefully handle activity and context changes. * - *

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} - * package instead. + *

Call {@link #registerWith} to use the stable {@code io.flutter.plugin.common} package instead. */ -public class WebViewFlutterPlugin implements FlutterPlugin { - - private FlutterCookieManager flutterCookieManager; +public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { + private FlutterPluginBinding pluginBinding; + private WebViewHostApiImpl webViewHostApi; + private JavaScriptChannelHostApiImpl javaScriptChannelHostApi; /** * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to @@ -26,7 +42,7 @@ public class WebViewFlutterPlugin implements FlutterPlugin { *

THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE * PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least * flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. Use the V1 embedding with {@link - * #registerWith(Registrar)} to use this plugin with older Flutter versions. + * #registerWith} to use this plugin with older Flutter versions. * *

Registration should eventually be handled automatically by v2 of the * GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694 @@ -38,36 +54,112 @@ public WebViewFlutterPlugin() {} * package. * *

Calling this automatically initializes the plugin. However plugins initialized this way - * won't react to changes in activity or context, unlike {@link CameraPlugin}. + * won't react to changes in activity or context, unlike {@link WebViewFlutterPlugin}. */ - @SuppressWarnings("deprecation") + @SuppressWarnings({"unused", "deprecation"}) public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(registrar.messenger(), registrar.view())); - new FlutterCookieManager(registrar.messenger()); + new WebViewFlutterPlugin() + .setUp( + registrar.messenger(), + registrar.platformViewRegistry(), + registrar.activity(), + registrar.view(), + new FlutterAssetManager.RegistrarFlutterAssetManager( + registrar.context().getAssets(), registrar)); + } + + private void setUp( + BinaryMessenger binaryMessenger, + PlatformViewRegistry viewRegistry, + Context context, + View containerView, + FlutterAssetManager flutterAssetManager) { + + InstanceManager instanceManager = new InstanceManager(); + + viewRegistry.registerViewFactory( + "plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager)); + + webViewHostApi = + new WebViewHostApiImpl( + instanceManager, new WebViewHostApiImpl.WebViewProxy(), context, containerView); + javaScriptChannelHostApi = + new JavaScriptChannelHostApiImpl( + instanceManager, + new JavaScriptChannelHostApiImpl.JavaScriptChannelCreator(), + new JavaScriptChannelFlutterApiImpl(binaryMessenger, instanceManager), + new Handler(context.getMainLooper())); + + WebViewHostApi.setup(binaryMessenger, webViewHostApi); + JavaScriptChannelHostApi.setup(binaryMessenger, javaScriptChannelHostApi); + WebViewClientHostApi.setup( + binaryMessenger, + new WebViewClientHostApiImpl( + instanceManager, + new WebViewClientHostApiImpl.WebViewClientCreator(), + new WebViewClientFlutterApiImpl(binaryMessenger, instanceManager))); + WebChromeClientHostApi.setup( + binaryMessenger, + new WebChromeClientHostApiImpl( + instanceManager, + new WebChromeClientHostApiImpl.WebChromeClientCreator(), + new WebChromeClientFlutterApiImpl(binaryMessenger, instanceManager))); + DownloadListenerHostApi.setup( + binaryMessenger, + new DownloadListenerHostApiImpl( + instanceManager, + new DownloadListenerHostApiImpl.DownloadListenerCreator(), + new DownloadListenerFlutterApiImpl(binaryMessenger, instanceManager))); + WebSettingsHostApi.setup( + binaryMessenger, + new WebSettingsHostApiImpl( + instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); + FlutterAssetManagerHostApi.setup( + binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); + CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); + WebStorageHostApi.setup( + binaryMessenger, + new WebStorageHostApiImpl(instanceManager, new WebStorageHostApiImpl.WebStorageCreator())); } @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + pluginBinding = binding; + setUp( + binding.getBinaryMessenger(), + binding.getPlatformViewRegistry(), + binding.getApplicationContext(), + null, + new FlutterAssetManager.PluginBindingFlutterAssetManager( + binding.getApplicationContext().getAssets(), binding.getFlutterAssets())); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} + + @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()); + } - flutterCookieManager.dispose(); - flutterCookieManager = null; + private void updateContext(Context context) { + webViewHostApi.setContext(context); + javaScriptChannelHostApi.setPlatformThreadHandler(new Handler(context.getMainLooper())); } } 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 new file mode 100644 index 000000000000..afc3efee80ff --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java @@ -0,0 +1,503 @@ +// Copyright 2013 The Flutter 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.annotation.SuppressLint; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.ReleasableWebViewClient; +import java.util.HashMap; +import java.util.Map; + +/** + * Host api implementation for {@link WebView}. + * + *

Handles creating {@link WebView}s that intercommunicate with a paired Dart object. + */ +public class WebViewHostApiImpl implements WebViewHostApi { + private final InstanceManager instanceManager; + private final WebViewProxy webViewProxy; + // Only used with WebView using virtual displays. + @Nullable private final View containerView; + + private Context context; + + /** Handles creating and calling static methods for {@link WebView}s. */ + public static class WebViewProxy { + /** + * Creates a {@link WebViewPlatformView}. + * + * @param context an Activity Context to access application assets + * @return the created {@link WebViewPlatformView} + */ + public WebViewPlatformView createWebView(Context context) { + return new WebViewPlatformView(context); + } + + /** + * Creates a {@link InputAwareWebViewPlatformView}. + * + * @param context an Activity Context to access application assets + * @param containerView parent View of the WebView + * @return the created {@link InputAwareWebViewPlatformView} + */ + public InputAwareWebViewPlatformView createInputAwareWebView( + Context context, @Nullable View containerView) { + return new InputAwareWebViewPlatformView(context, containerView); + } + + /** + * Forwards call to {@link WebView#setWebContentsDebuggingEnabled}. + * + * @param enabled whether debugging should be enabled + */ + public void setWebContentsDebuggingEnabled(boolean enabled) { + WebView.setWebContentsDebuggingEnabled(enabled); + } + } + + private static class ReleasableValue { + @Nullable private T value; + + ReleasableValue() {} + + ReleasableValue(@Nullable T value) { + this.value = value; + } + + void set(@Nullable T newValue) { + release(); + value = newValue; + } + + @Nullable + T get() { + return value; + } + + void release() { + if (value != null) { + value.release(); + } + value = null; + } + } + + /** Implementation of {@link WebView} that can be used as a Flutter {@link PlatformView}s. */ + public static class WebViewPlatformView extends WebView implements PlatformView, Releasable { + private final ReleasableValue + currentWebViewClient = new ReleasableValue<>(); + private final ReleasableValue currentDownloadListener = + new ReleasableValue<>(); + private final ReleasableValue currentWebChromeClient = + new ReleasableValue<>(); + private final Map> javaScriptInterfaces = + new HashMap<>(); + + /** + * Creates a {@link WebViewPlatformView}. + * + * @param context an Activity Context to access application assets. This value cannot be null. + */ + public WebViewPlatformView(Context context) { + super(context); + } + + @Override + public View getView() { + return this; + } + + @Override + public void dispose() { + destroy(); + } + + @Override + public void setWebViewClient(WebViewClient webViewClient) { + super.setWebViewClient(webViewClient); + currentWebViewClient.set((ReleasableWebViewClient) webViewClient); + + final WebChromeClientImpl webChromeClient = currentWebChromeClient.get(); + if (webChromeClient != null) { + ((WebChromeClientImpl) webChromeClient).setWebViewClient(webViewClient); + } + } + + @Override + public void setDownloadListener(DownloadListener listener) { + super.setDownloadListener(listener); + currentDownloadListener.set((DownloadListenerImpl) listener); + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + currentWebChromeClient.set((WebChromeClientImpl) client); + } + + @SuppressLint("JavascriptInterface") + @Override + public void addJavascriptInterface(Object object, String name) { + super.addJavascriptInterface(object, name); + if (object instanceof JavaScriptChannel) { + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + if (javaScriptChannel != null && javaScriptChannel.get() != object) { + javaScriptChannel.release(); + } + javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object)); + } + } + + @Override + public void removeJavascriptInterface(@NonNull String name) { + super.removeJavascriptInterface(name); + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + javaScriptChannel.release(); + javaScriptInterfaces.remove(name); + } + + @Override + public void release() { + currentWebViewClient.release(); + currentDownloadListener.release(); + currentWebChromeClient.release(); + for (ReleasableValue channel : javaScriptInterfaces.values()) { + channel.release(); + } + javaScriptInterfaces.clear(); + } + } + + /** + * Implementation of {@link InputAwareWebView} that can be used as a Flutter {@link + * PlatformView}s. + */ + @SuppressLint("ViewConstructor") + public static class InputAwareWebViewPlatformView extends InputAwareWebView + implements PlatformView, Releasable { + private final ReleasableValue + currentWebViewClient = new ReleasableValue<>(); + private final ReleasableValue currentDownloadListener = + new ReleasableValue<>(); + private final ReleasableValue currentWebChromeClient = + new ReleasableValue<>(); + private final Map> javaScriptInterfaces = + new HashMap<>(); + + /** + * Creates a {@link InputAwareWebViewPlatformView}. + * + * @param context an Activity Context to access application assets. This value cannot be null. + */ + public InputAwareWebViewPlatformView(Context context, View containerView) { + super(context, containerView); + } + + @Override + public View getView() { + return this; + } + + @Override + public void onFlutterViewAttached(@NonNull View flutterView) { + setContainerView(flutterView); + } + + @Override + public void onFlutterViewDetached() { + setContainerView(null); + } + + @Override + public void dispose() { + super.dispose(); + destroy(); + } + + @Override + public void onInputConnectionLocked() { + lockInputConnection(); + } + + @Override + public void onInputConnectionUnlocked() { + unlockInputConnection(); + } + + @Override + public void setWebViewClient(WebViewClient webViewClient) { + super.setWebViewClient(webViewClient); + currentWebViewClient.set((ReleasableWebViewClient) webViewClient); + + final WebChromeClientImpl webChromeClient = currentWebChromeClient.get(); + if (webChromeClient != null) { + webChromeClient.setWebViewClient(webViewClient); + } + } + + @Override + public void setDownloadListener(DownloadListener listener) { + super.setDownloadListener(listener); + currentDownloadListener.set((DownloadListenerImpl) listener); + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + currentWebChromeClient.set((WebChromeClientImpl) client); + } + + @SuppressLint("JavascriptInterface") + @Override + public void addJavascriptInterface(Object object, String name) { + super.addJavascriptInterface(object, name); + if (object instanceof JavaScriptChannel) { + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + if (javaScriptChannel != null && javaScriptChannel.get() != object) { + javaScriptChannel.release(); + } + javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object)); + } + } + + @Override + public void removeJavascriptInterface(@NonNull String name) { + super.removeJavascriptInterface(name); + final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); + javaScriptChannel.release(); + javaScriptInterfaces.remove(name); + } + + @Override + public void release() { + currentWebViewClient.release(); + currentDownloadListener.release(); + currentWebChromeClient.release(); + for (ReleasableValue channel : javaScriptInterfaces.values()) { + channel.release(); + } + javaScriptInterfaces.clear(); + } + } + + /** + * Creates a host API that handles creating {@link WebView}s and invoking its methods. + * + * @param instanceManager maintains instances stored to communicate with Dart objects + * @param webViewProxy handles creating {@link WebView}s and calling its static methods + * @param context an Activity Context to access application assets. This value cannot be null. + * @param containerView parent of the webView + */ + public WebViewHostApiImpl( + InstanceManager instanceManager, + WebViewProxy webViewProxy, + Context context, + @Nullable View containerView) { + this.instanceManager = instanceManager; + this.webViewProxy = webViewProxy; + this.context = context; + this.containerView = containerView; + } + + /** + * Sets the context to construct {@link WebView}s. + * + * @param context the new context. + */ + public void setContext(Context context) { + this.context = context; + } + + @Override + public void create(Long instanceId, Boolean useHybridComposition) { + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + + final WebView webView = + useHybridComposition + ? webViewProxy.createWebView(context) + : webViewProxy.createInputAwareWebView(context, containerView); + + displayListenerProxy.onPostWebViewInitialization(displayManager); + instanceManager.addInstance(webView, instanceId); + } + + @Override + public void dispose(Long instanceId) { + final WebView instance = (WebView) instanceManager.getInstance(instanceId); + if (instance != null) { + ((Releasable) instance).release(); + instanceManager.removeInstance(instance); + } + } + + @Override + public void loadData(Long instanceId, String data, String mimeType, String encoding) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.loadData(data, mimeType, encoding); + } + + @Override + public void loadDataWithBaseUrl( + Long instanceId, + String baseUrl, + String data, + String mimeType, + String encoding, + String historyUrl) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + } + + @Override + public void loadUrl(Long instanceId, String url, Map headers) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.loadUrl(url, headers); + } + + @Override + public void postUrl(Long instanceId, String url, byte[] data) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.postUrl(url, data); + } + + @Override + public String getUrl(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.getUrl(); + } + + @Override + public Boolean canGoBack(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.canGoBack(); + } + + @Override + public Boolean canGoForward(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.canGoForward(); + } + + @Override + public void goBack(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.goBack(); + } + + @Override + public void goForward(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.goForward(); + } + + @Override + public void reload(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.reload(); + } + + @Override + public void clearCache(Long instanceId, Boolean includeDiskFiles) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.clearCache(includeDiskFiles); + } + + @Override + public void evaluateJavascript( + Long instanceId, String javascriptString, GeneratedAndroidWebView.Result result) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.evaluateJavascript(javascriptString, result::success); + } + + @Override + public String getTitle(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return webView.getTitle(); + } + + @Override + public void scrollTo(Long instanceId, Long x, Long y) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.scrollTo(x.intValue(), y.intValue()); + } + + @Override + public void scrollBy(Long instanceId, Long x, Long y) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.scrollBy(x.intValue(), y.intValue()); + } + + @Override + public Long getScrollX(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return (long) webView.getScrollX(); + } + + @Override + public Long getScrollY(Long instanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + return (long) webView.getScrollY(); + } + + @Override + public void setWebContentsDebuggingEnabled(Boolean enabled) { + webViewProxy.setWebContentsDebuggingEnabled(enabled); + } + + @Override + public void setWebViewClient(Long instanceId, Long webViewClientInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setWebViewClient((WebViewClient) instanceManager.getInstance(webViewClientInstanceId)); + } + + @Override + public void addJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + final JavaScriptChannel javaScriptChannel = + (JavaScriptChannel) instanceManager.getInstance(javaScriptChannelInstanceId); + webView.addJavascriptInterface(javaScriptChannel, javaScriptChannel.javaScriptChannelName); + } + + @Override + public void removeJavaScriptChannel(Long instanceId, Long javaScriptChannelInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + final JavaScriptChannel javaScriptChannel = + (JavaScriptChannel) instanceManager.getInstance(javaScriptChannelInstanceId); + webView.removeJavascriptInterface(javaScriptChannel.javaScriptChannelName); + } + + @Override + public void setDownloadListener(Long instanceId, Long listenerInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setDownloadListener((DownloadListener) instanceManager.getInstance(listenerInstanceId)); + } + + @Override + public void setWebChromeClient(Long instanceId, Long clientInstanceId) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(clientInstanceId)); + } + + @Override + public void setBackgroundColor(Long instanceId, Long color) { + final WebView webView = (WebView) instanceManager.getInstance(instanceId); + webView.setBackgroundColor(color.intValue()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java new file mode 100644 index 000000000000..4a90e394e259 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/android/util/LongSparseArray.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package android.util; + +import java.util.HashMap; + +// Creates an implementation of LongSparseArray that can be used with unittests and the JVM. +// Typically android.util.LongSparseArray does nothing when not used with an Android environment. +public class LongSparseArray { + private final HashMap mHashMap; + + public LongSparseArray() { + mHashMap = new HashMap<>(); + } + + public void append(long key, E value) { + mHashMap.put(key, value); + } + + public E get(long key) { + return mHashMap.get(key); + } + + public void remove(long key) { + mHashMap.remove(key); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java new file mode 100644 index 000000000000..6daeb1be7f63 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Build; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import io.flutter.plugins.webviewflutter.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class CookieManagerHostApiImplTest { + + private CookieManager cookieManager; + private MockedStatic staticMockCookieManager; + + @Before + public void setup() { + staticMockCookieManager = mockStatic(CookieManager.class); + cookieManager = mock(CookieManager.class); + when(CookieManager.getInstance()).thenReturn(cookieManager); + when(cookieManager.hasCookies()).thenReturn(true); + doAnswer( + answer -> { + ((ValueCallback) answer.getArgument(0)).onReceiveValue(true); + return null; + }) + .when(cookieManager) + .removeAllCookies(any()); + } + + @After + public void tearDown() { + staticMockCookieManager.close(); + } + + @Test + public void setCookieShouldCallSetCookie() { + // Setup + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.setCookie("flutter.dev", "foo=bar; path=/"); + // Verify + verify(cookieManager).setCookie("flutter.dev", "foo=bar; path=/"); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookiesOnAndroidLAbove() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookies(any()); + verify(result).success(true); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookieBelowAndroidL() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT_WATCH); + GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookie(); + verify(result).success(true); + } +} 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 new file mode 100644 index 000000000000..239119375e83 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import android.webkit.DownloadListener; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerCreator; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +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 DownloadListenerTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public DownloadListenerFlutterApiImpl mockFlutterApi; + + InstanceManager instanceManager; + DownloadListenerHostApiImpl hostApiImpl; + DownloadListenerImpl downloadListener; + + @Before + public void setUp() { + instanceManager = new InstanceManager(); + + final DownloadListenerCreator downloadListenerCreator = + new DownloadListenerCreator() { + @Override + public DownloadListenerImpl createDownloadListener( + DownloadListenerFlutterApiImpl flutterApi) { + downloadListener = super.createDownloadListener(flutterApi); + return downloadListener; + } + }; + + hostApiImpl = + new DownloadListenerHostApiImpl(instanceManager, downloadListenerCreator, mockFlutterApi); + hostApiImpl.create(0L); + } + + @Test + public void postMessage() { + downloadListener.onDownloadStart( + "https://www.google.com", "userAgent", "contentDisposition", "mimetype", 54); + verify(mockFlutterApi) + .onDownloadStart( + eq(downloadListener), + eq("https://www.google.com"), + eq("userAgent"), + eq("contentDisposition"), + eq("mimetype"), + eq(54L), + any()); + + reset(mockFlutterApi); + downloadListener.release(); + downloadListener.onDownloadStart("", "", "", "", 23); + verify(mockFlutterApi, never()) + .onDownloadStart((DownloadListener) any(), any(), any(), any(), any(), eq(23), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java new file mode 100644 index 000000000000..f530365a9334 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterAssetManagerHostApiImplTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class FlutterAssetManagerHostApiImplTest { + @Mock FlutterAssetManager mockFlutterAssetManager; + + FlutterAssetManagerHostApiImpl testFlutterAssetManagerHostApiImpl; + + @Before + public void setUp() { + mockFlutterAssetManager = mock(FlutterAssetManager.class); + + testFlutterAssetManagerHostApiImpl = + new FlutterAssetManagerHostApiImpl(mockFlutterAssetManager); + } + + @Test + public void list() { + try { + when(mockFlutterAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + List actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void list_returns_empty_list_when_no_results() { + try { + when(mockFlutterAssetManager.list("test/path")).thenReturn(null); + List actualFilePaths = testFlutterAssetManagerHostApiImpl.list("test/path"); + verify(mockFlutterAssetManager).list("test/path"); + assertArrayEquals(new String[] {}, actualFilePaths.toArray()); + } catch (IOException ex) { + fail(); + } + } + + @Test(expected = RuntimeException.class) + public void list_should_convert_io_exception_to_runtime_exception() { + try { + when(mockFlutterAssetManager.list("test/path")).thenThrow(new IOException()); + testFlutterAssetManagerHostApiImpl.list("test/path"); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void getAssetFilePathByName() { + when(mockFlutterAssetManager.getAssetFilePathByName("index.html")) + .thenReturn("flutter_assets/index.html"); + String filePath = testFlutterAssetManagerHostApiImpl.getAssetFilePathByName("index.html"); + verify(mockFlutterAssetManager).getAssetFilePathByName("index.html"); + assertEquals("flutter_assets/index.html", filePath); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java deleted file mode 100644 index 2c918584ba83..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java +++ /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. - -package io.flutter.plugins.webviewflutter; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.webkit.WebView; -import org.junit.Before; -import org.junit.Test; - -public class FlutterDownloadListenerTest { - private FlutterWebViewClient webViewClient; - private WebView webView; - - @Before - public void before() { - webViewClient = mock(FlutterWebViewClient.class); - webView = mock(WebView.class); - } - - @Test - public void onDownloadStart_should_notify_webViewClient() { - String url = "testurl.com"; - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); - } - - @Test - public void onDownloadStart_should_pass_webView() { - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.setWebView(webView); - downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(eq(webView), anyString()); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java deleted file mode 100644 index 86346ac08f16..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java +++ /dev/null @@ -1,60 +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.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.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import android.webkit.WebView; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -public class FlutterWebViewClientTest { - - MethodChannel mockMethodChannel; - WebView mockWebView; - - @Before - public void before() { - mockMethodChannel = mock(MethodChannel.class); - mockWebView = mock(WebView.class); - } - - @Test - public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(true); - - client.notifyDownload(mockWebView, url); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); - verify(mockMethodChannel) - .invokeMethod( - eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); - HashMap map = (HashMap) argumentCaptor.getValue(); - assertEquals(map.get("url"), url); - assertEquals(map.get("isForMainFrame"), true); - } - - @Test - public void - notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(false); - - client.notifyDownload(mockWebView, url); - verifyNoInteractions(mockMethodChannel); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java deleted file mode 100644 index 56d9db1ee493..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ /dev/null @@ -1,66 +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.webviewflutter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; - -public class FlutterWebViewTest { - private WebChromeClient mockWebChromeClient; - private DownloadListener mockDownloadListener; - private WebViewBuilder mockWebViewBuilder; - private WebView mockWebView; - - @Before - public void before() { - mockWebChromeClient = mock(WebChromeClient.class); - mockWebViewBuilder = mock(WebViewBuilder.class); - mockWebView = mock(WebView.class); - mockDownloadListener = mock(DownloadListener.class); - - when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) - .thenReturn(mockWebViewBuilder); - - when(mockWebViewBuilder.build()).thenReturn(mockWebView); - } - - @Test - public void createWebView_should_create_webview_with_default_configuration() { - FlutterWebView.createWebView( - mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); - - verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); - verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); - verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); - verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); - } - - private Map createParameterMap(boolean usesHybridComposition) { - Map params = new HashMap<>(); - params.put("usesHybridComposition", usesHybridComposition); - - return params; - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.java new file mode 100644 index 000000000000..0eb078d5a7cf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InputAwareWebViewTest.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.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.view.View; +import org.junit.Test; + +public class InputAwareWebViewTest { + static class TestView extends View { + Runnable postAction; + + public TestView(Context context) { + super(context); + } + + @Override + public boolean post(Runnable action) { + postAction = action; + return true; + } + } + + @Test + public void runnableChecksContainerViewIsNull() { + final Context mockContext = mock(Context.class); + + final TestView containerView = new TestView(mockContext); + final InputAwareWebView inputAwareWebView = new InputAwareWebView(mockContext, containerView); + + final View mockProxyAdapterView = mock(View.class); + + inputAwareWebView.setInputConnectionTarget(mockProxyAdapterView); + inputAwareWebView.setContainerView(null); + + assertNotNull(containerView.postAction); + containerView.postAction.run(); + verify(mockProxyAdapterView, never()).onWindowFocusChanged(anyBoolean()); + } +} 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 new file mode 100644 index 000000000000..3de81da81bec --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import android.os.Handler; +import io.flutter.plugins.webviewflutter.JavaScriptChannelHostApiImpl.JavaScriptChannelCreator; +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 JavaScriptChannelTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public JavaScriptChannelFlutterApiImpl mockFlutterApi; + + InstanceManager instanceManager; + JavaScriptChannelHostApiImpl hostApiImpl; + JavaScriptChannel javaScriptChannel; + + @Before + public void setUp() { + instanceManager = new InstanceManager(); + + final JavaScriptChannelCreator javaScriptChannelCreator = + new JavaScriptChannelCreator() { + @Override + public JavaScriptChannel createJavaScriptChannel( + JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi, + String channelName, + Handler platformThreadHandler) { + javaScriptChannel = + super.createJavaScriptChannel( + javaScriptChannelFlutterApi, channelName, platformThreadHandler); + return javaScriptChannel; + } + }; + + hostApiImpl = + new JavaScriptChannelHostApiImpl( + instanceManager, javaScriptChannelCreator, mockFlutterApi, new Handler()); + hostApiImpl.create(0L, "aChannelName"); + } + + @Test + public void postMessage() { + javaScriptChannel.postMessage("A message post."); + verify(mockFlutterApi).postMessage(eq(javaScriptChannel), eq("A message post."), any()); + + reset(mockFlutterApi); + javaScriptChannel.release(); + javaScriptChannel.postMessage("a message"); + verify(mockFlutterApi, never()).postMessage((JavaScriptChannel) any(), any(), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java new file mode 100644 index 000000000000..1f556b7bd486 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PluginBindingFlutterAssetManagerTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.PluginBindingFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class PluginBindingFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock FlutterAssets mockFlutterAssets; + + PluginBindingFlutterAssetManager tesPluginBindingFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockFlutterAssets = mock(FlutterAssets.class); + + tesPluginBindingFlutterAssetManager = + new PluginBindingFlutterAssetManager(mockAssetManager, mockFlutterAssets); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = tesPluginBindingFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + tesPluginBindingFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockFlutterAssets).getAssetFilePathByName("sample_movie.mp4"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java new file mode 100644 index 000000000000..86b0fb5432b9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/RegistrarFlutterAssetManagerTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.res.AssetManager; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugins.webviewflutter.FlutterAssetManager.RegistrarFlutterAssetManager; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +@SuppressWarnings("deprecation") +public class RegistrarFlutterAssetManagerTest { + @Mock AssetManager mockAssetManager; + @Mock Registrar mockRegistrar; + + RegistrarFlutterAssetManager testRegistrarFlutterAssetManager; + + @Before + public void setUp() { + mockAssetManager = mock(AssetManager.class); + mockRegistrar = mock(Registrar.class); + + testRegistrarFlutterAssetManager = + new RegistrarFlutterAssetManager(mockAssetManager, mockRegistrar); + } + + @Test + public void list() { + try { + when(mockAssetManager.list("test/path")) + .thenReturn(new String[] {"index.html", "styles.css"}); + String[] actualFilePaths = testRegistrarFlutterAssetManager.list("test/path"); + verify(mockAssetManager).list("test/path"); + assertArrayEquals(new String[] {"index.html", "styles.css"}, actualFilePaths); + } catch (IOException ex) { + fail(); + } + } + + @Test + public void registrar_getAssetFilePathByName() { + testRegistrarFlutterAssetManager.getAssetFilePathByName("sample_movie.mp4"); + verify(mockRegistrar).lookupKeyForAsset("sample_movie.mp4"); + } +} 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 new file mode 100644 index 000000000000..63cd31043799 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertNotNull; +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.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.os.Message; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebView.WebViewTransport; +import android.webkit.WebViewClient; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientCreator; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class WebChromeClientTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebChromeClientFlutterApiImpl mockFlutterApi; + + @Mock public WebView mockWebView; + + @Mock public WebViewClient mockWebViewClient; + + InstanceManager instanceManager; + WebChromeClientHostApiImpl hostApiImpl; + WebChromeClientImpl webChromeClient; + + @Before + public void setUp() { + instanceManager = new InstanceManager(); + instanceManager.addInstance(mockWebView, 0L); + instanceManager.addInstance(mockWebViewClient, 1L); + + final WebChromeClientCreator webChromeClientCreator = + new WebChromeClientCreator() { + @Override + public WebChromeClientImpl createWebChromeClient( + WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + webChromeClient = super.createWebChromeClient(flutterApi, webViewClient); + return webChromeClient; + } + }; + + hostApiImpl = + new WebChromeClientHostApiImpl(instanceManager, webChromeClientCreator, mockFlutterApi); + hostApiImpl.create(2L, 1L); + } + + @Test + public void onProgressChanged() { + webChromeClient.onProgressChanged(mockWebView, 23); + verify(mockFlutterApi).onProgressChanged(eq(webChromeClient), eq(mockWebView), eq(23L), any()); + + reset(mockFlutterApi); + webChromeClient.release(); + webChromeClient.onProgressChanged(mockWebView, 11); + verify(mockFlutterApi, never()).onProgressChanged((WebChromeClient) any(), any(), any(), any()); + } + + @Test + public void onCreateWindow() { + final WebView mockOnCreateWindowWebView = mock(WebView.class); + + // Create a fake message to transport requests to onCreateWindowWebView. + final Message message = new Message(); + message.obj = mock(WebViewTransport.class); + + assertTrue(webChromeClient.onCreateWindow(mockWebView, message, mockOnCreateWindowWebView)); + + /// Capture the WebViewClient used with onCreateWindow WebView. + final ArgumentCaptor webViewClientCaptor = + ArgumentCaptor.forClass(WebViewClient.class); + verify(mockOnCreateWindowWebView).setWebViewClient(webViewClientCaptor.capture()); + final WebViewClient onCreateWindowWebViewClient = webViewClientCaptor.getValue(); + assertNotNull(onCreateWindowWebViewClient); + + /// Create a WebResourceRequest with a Uri. + final WebResourceRequest mockRequest = mock(WebResourceRequest.class); + when(mockRequest.getUrl()).thenReturn(mock(Uri.class)); + when(mockRequest.getUrl().toString()).thenReturn("https://www.google.com"); + + // Test when the forwarding WebViewClient is overriding all url loading. + when(mockWebViewClient.shouldOverrideUrlLoading(any(), any(WebResourceRequest.class))) + .thenReturn(true); + assertTrue( + onCreateWindowWebViewClient.shouldOverrideUrlLoading( + mockOnCreateWindowWebView, mockRequest)); + verify(mockWebView, never()).loadUrl(any()); + + // Test when the forwarding WebViewClient is NOT overriding all url loading. + when(mockWebViewClient.shouldOverrideUrlLoading(any(), any(WebResourceRequest.class))) + .thenReturn(false); + assertTrue( + onCreateWindowWebViewClient.shouldOverrideUrlLoading( + mockOnCreateWindowWebView, mockRequest)); + verify(mockWebView).loadUrl("https://www.google.com"); + } +} 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 new file mode 100644 index 000000000000..8ef32ddcb4ca --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebSettingsTest.java @@ -0,0 +1,103 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebSettings; +import io.flutter.plugins.webviewflutter.WebSettingsHostApiImpl.WebSettingsCreator; +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 WebSettingsTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebSettings mockWebSettings; + + @Mock WebSettingsCreator mockWebSettingsCreator; + + InstanceManager testInstanceManager; + WebSettingsHostApiImpl testHostApiImpl; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + when(mockWebSettingsCreator.createWebSettings(any())).thenReturn(mockWebSettings); + testHostApiImpl = new WebSettingsHostApiImpl(testInstanceManager, mockWebSettingsCreator); + testHostApiImpl.create(0L, 0L); + } + + @Test + public void setDomStorageEnabled() { + testHostApiImpl.setDomStorageEnabled(0L, true); + verify(mockWebSettings).setDomStorageEnabled(true); + } + + @Test + public void setJavaScriptCanOpenWindowsAutomatically() { + testHostApiImpl.setJavaScriptCanOpenWindowsAutomatically(0L, false); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); + } + + @Test + public void setSupportMultipleWindows() { + testHostApiImpl.setSupportMultipleWindows(0L, true); + verify(mockWebSettings).setSupportMultipleWindows(true); + } + + @Test + public void setJavaScriptEnabled() { + testHostApiImpl.setJavaScriptEnabled(0L, false); + verify(mockWebSettings).setJavaScriptEnabled(false); + } + + @Test + public void setUserAgentString() { + testHostApiImpl.setUserAgentString(0L, "hello"); + verify(mockWebSettings).setUserAgentString("hello"); + } + + @Test + public void setMediaPlaybackRequiresUserGesture() { + testHostApiImpl.setMediaPlaybackRequiresUserGesture(0L, false); + verify(mockWebSettings).setMediaPlaybackRequiresUserGesture(false); + } + + @Test + public void setSupportZoom() { + testHostApiImpl.setSupportZoom(0L, true); + verify(mockWebSettings).setSupportZoom(true); + } + + @Test + public void setLoadWithOverviewMode() { + testHostApiImpl.setLoadWithOverviewMode(0L, false); + verify(mockWebSettings).setLoadWithOverviewMode(false); + } + + @Test + public void setUseWideViewPort() { + testHostApiImpl.setUseWideViewPort(0L, true); + verify(mockWebSettings).setUseWideViewPort(true); + } + + @Test + public void setDisplayZoomControls() { + testHostApiImpl.setDisplayZoomControls(0L, false); + verify(mockWebSettings).setDisplayZoomControls(false); + } + + @Test + public void setBuiltInZoomControls() { + testHostApiImpl.setBuiltInZoomControls(0L, true); + verify(mockWebSettings).setBuiltInZoomControls(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..e2845c2842a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebStorageHostApiImplTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebStorage; +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 = new InstanceManager(); + when(mockWebStorageCreator.createWebStorage()).thenReturn(mockWebStorage); + testHostApiImpl = new WebStorageHostApiImpl(testInstanceManager, mockWebStorageCreator); + testHostApiImpl.create(0L); + } + + @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/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java deleted file mode 100644 index 423cb210c392..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; -import java.io.IOException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; -import org.mockito.MockedStatic.Verification; - -public class WebViewBuilderTest { - private Context mockContext; - private View mockContainerView; - private WebView mockWebView; - private MockedStatic mockedStaticWebViewFactory; - - @Before - public void before() { - mockContext = mock(Context.class); - mockContainerView = mock(View.class); - mockWebView = mock(WebView.class); - mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); - - mockedStaticWebViewFactory - .when( - new Verification() { - @Override - public void apply() { - WebViewFactory.create(mockContext, false, mockContainerView); - } - }) - .thenReturn(mockWebView); - } - - @After - public void after() { - mockedStaticWebViewFactory.close(); - } - - @Test - public void ctor_test() { - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - assertNotNull(builder); - } - - @Test - public void build_should_set_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - DownloadListener mockDownloadListener = mock(DownloadListener.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = - new WebViewBuilder(mockContext, mockContainerView) - .setDomStorageEnabled(true) - .setJavaScriptCanOpenWindowsAutomatically(true) - .setSupportMultipleWindows(true) - .setWebChromeClient(mockWebChromeClient) - .setDownloadListener(mockDownloadListener); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(true); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebSettings).setSupportMultipleWindows(true); - verify(mockWebView).setWebChromeClient(mockWebChromeClient); - verify(mockWebView).setDownloadListener(mockDownloadListener); - } - - @Test - public void build_should_use_default_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(false); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); - verify(mockWebSettings).setSupportMultipleWindows(false); - verify(mockWebView).setWebChromeClient(null); - verify(mockWebView).setDownloadListener(null); - } -} 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 new file mode 100644 index 000000000000..c2abd25c5a66 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java @@ -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. + +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.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 WebViewClientTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebViewClientFlutterApiImpl mockFlutterApi; + + @Mock public WebView mockWebView; + + InstanceManager instanceManager; + WebViewClientHostApiImpl hostApiImpl; + WebViewClientCompatImpl webViewClient; + + @Before + public void setUp() { + instanceManager = new InstanceManager(); + instanceManager.addInstance(mockWebView, 0L); + + final WebViewClientCreator webViewClientCreator = + new WebViewClientCreator() { + @Override + public WebViewClient createWebViewClient( + WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + webViewClient = + (WebViewClientCompatImpl) + super.createWebViewClient(flutterApi, shouldOverrideUrlLoading); + return webViewClient; + } + }; + + hostApiImpl = + new WebViewClientHostApiImpl(instanceManager, webViewClientCreator, mockFlutterApi); + hostApiImpl.create(1L, true); + } + + @Test + public void onPageStarted() { + webViewClient.onPageStarted(mockWebView, "https://www.google.com", null); + verify(mockFlutterApi) + .onPageStarted(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any()); + + reset(mockFlutterApi); + webViewClient.release(); + webViewClient.onPageStarted(mockWebView, "", null); + verify(mockFlutterApi, never()).onPageStarted((WebViewClient) any(), any(), any(), any()); + } + + @Test + public void onReceivedError() { + webViewClient.onReceivedError(mockWebView, 32, "description", "https://www.google.com"); + verify(mockFlutterApi) + .onReceivedError( + eq(webViewClient), + eq(mockWebView), + eq(32L), + eq("description"), + eq("https://www.google.com"), + any()); + + reset(mockFlutterApi); + webViewClient.release(); + webViewClient.onReceivedError(mockWebView, 33, "", ""); + verify(mockFlutterApi, never()) + .onReceivedError((WebViewClient) any(), any(), any(), any(), any(), any()); + } + + @Test + public void urlLoading() { + webViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com"); + verify(mockFlutterApi) + .urlLoading(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any()); + + reset(mockFlutterApi); + webViewClient.release(); + 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/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java index 131a5a3eb53a..5be39ab963a3 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 @@ -5,45 +5,350 @@ package io.flutter.plugins.webviewflutter; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.Context; +import android.webkit.DownloadListener; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; import android.webkit.WebViewClient; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientImpl; +import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.InputAwareWebViewPlatformView; +import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; public class WebViewTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public WebViewPlatformView mockWebView; + + @Mock WebViewHostApiImpl.WebViewProxy mockWebViewProxy; + + @Mock Context mockContext; + + InstanceManager testInstanceManager; + WebViewHostApiImpl testHostApiImpl; + + @Before + public void setUp() { + testInstanceManager = new InstanceManager(); + when(mockWebViewProxy.createWebView(mockContext)).thenReturn(mockWebView); + testHostApiImpl = + new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext, null); + testHostApiImpl.create(0L, true); + } + + @Test + public void releaseWebView() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.release(); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseWebViewDependents() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + // Release should be called on the object added above. + webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel"); + verify(mockJavaScriptChannel).release(); + + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.setDownloadListener(null); + webView.removeJavascriptInterface("jchannel"); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel2).release(); + } + + @Test + public void releaseInputAwareWebView() { + final InputAwareWebViewPlatformView webView = + new InputAwareWebViewPlatformView(mockContext, null); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.release(); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseInputAwareWebViewDependents() { + final InputAwareWebViewPlatformView webView = + new InputAwareWebViewPlatformView(mockContext, null); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + // Release should be called on the object added above. + webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel"); + verify(mockJavaScriptChannel).release(); + + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.setDownloadListener(null); + webView.removeJavascriptInterface("jchannel"); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel2).release(); + } + + @Test + public void loadData() { + testHostApiImpl.loadData( + 0L, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", "text/plain", "base64"); + verify(mockWebView) + .loadData("VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", "text/plain", "base64"); + } + + @Test + public void loadDataWithNullValues() { + testHostApiImpl.loadData(0L, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null); + verify(mockWebView).loadData("VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null); + } + + @Test + public void loadDataWithBaseUrl() { + testHostApiImpl.loadDataWithBaseUrl( + 0L, + "https://flutter.dev", + "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", + "text/plain", + "base64", + "about:blank"); + verify(mockWebView) + .loadDataWithBaseURL( + "https://flutter.dev", + "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", + "text/plain", + "base64", + "about:blank"); + } + + @Test + public void loadDataWithBaseUrlAndNullValues() { + testHostApiImpl.loadDataWithBaseUrl( + 0L, null, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null, null); + verify(mockWebView) + .loadDataWithBaseURL(null, "VGhpcyBkYXRhIGlzIGJhc2U2NCBlbmNvZGVkLg==", null, null, null); + } + + @Test + public void loadUrl() { + testHostApiImpl.loadUrl(0L, "https://www.google.com", new HashMap<>()); + verify(mockWebView).loadUrl("https://www.google.com", new HashMap<>()); + } + + @Test + public void postUrl() { + testHostApiImpl.postUrl(0L, "https://www.google.com", new byte[] {0x01, 0x02}); + verify(mockWebView).postUrl("https://www.google.com", new byte[] {0x01, 0x02}); + } + + @Test + public void getUrl() { + when(mockWebView.getUrl()).thenReturn("https://www.google.com"); + assertEquals(testHostApiImpl.getUrl(0L), "https://www.google.com"); + } + + @Test + public void canGoBack() { + when(mockWebView.canGoBack()).thenReturn(true); + assertEquals(testHostApiImpl.canGoBack(0L), true); + } + @Test - public void errorCodes() { - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), - "authentication"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), - "failedSslHandshake"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), - "proxyAuthentication"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), - "tooManyRequests"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), - "unsafeResource"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), - "unsupportedAuthScheme"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), - "unsupportedScheme"); + public void canGoForward() { + when(mockWebView.canGoForward()).thenReturn(false); + assertEquals(testHostApiImpl.canGoForward(0L), false); + } + + @Test + public void goBack() { + testHostApiImpl.goBack(0L); + verify(mockWebView).goBack(); + } + + @Test + public void goForward() { + testHostApiImpl.goForward(0L); + verify(mockWebView).goForward(); + } + + @Test + public void reload() { + testHostApiImpl.reload(0L); + verify(mockWebView).reload(); + } + + @Test + public void clearCache() { + testHostApiImpl.clearCache(0L, false); + verify(mockWebView).clearCache(false); + } + + @Test + public void evaluateJavaScript() { + final String[] successValue = new String[1]; + testHostApiImpl.evaluateJavascript( + 0L, + "2 + 2", + new GeneratedAndroidWebView.Result() { + @Override + public void success(String result) { + successValue[0] = result; + } + + @Override + public void error(Throwable error) {} + }); + + @SuppressWarnings("unchecked") + final ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(ValueCallback.class); + verify(mockWebView).evaluateJavascript(eq("2 + 2"), callbackCaptor.capture()); + + callbackCaptor.getValue().onReceiveValue("da result"); + assertEquals(successValue[0], "da result"); + } + + @Test + public void getTitle() { + when(mockWebView.getTitle()).thenReturn("My title"); + assertEquals(testHostApiImpl.getTitle(0L), "My title"); + } + + @Test + public void scrollTo() { + testHostApiImpl.scrollTo(0L, 12L, 13L); + verify(mockWebView).scrollTo(12, 13); + } + + @Test + public void scrollBy() { + testHostApiImpl.scrollBy(0L, 15L, 23L); + verify(mockWebView).scrollBy(15, 23); + } + + @Test + public void getScrollX() { + when(mockWebView.getScrollX()).thenReturn(55); + assertEquals((long) testHostApiImpl.getScrollX(0L), 55); + } + + @Test + public void getScrollY() { + when(mockWebView.getScrollY()).thenReturn(23); + assertEquals((long) testHostApiImpl.getScrollY(0L), 23); + } + + @Test + public void setWebViewClient() { + final WebViewClient mockWebViewClient = mock(WebViewClient.class); + testInstanceManager.addInstance(mockWebViewClient, 1L); + + testHostApiImpl.setWebViewClient(0L, 1L); + verify(mockWebView).setWebViewClient(mockWebViewClient); + } + + @Test + public void addJavaScriptChannel() { + final JavaScriptChannel javaScriptChannel = + new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null); + testInstanceManager.addInstance(javaScriptChannel, 1L); + + testHostApiImpl.addJavaScriptChannel(0L, 1L); + verify(mockWebView).addJavascriptInterface(javaScriptChannel, "aName"); + } + + @Test + public void removeJavaScriptChannel() { + final JavaScriptChannel javaScriptChannel = + new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null); + testInstanceManager.addInstance(javaScriptChannel, 1L); + + testHostApiImpl.removeJavaScriptChannel(0L, 1L); + verify(mockWebView).removeJavascriptInterface("aName"); + } + + @Test + public void setDownloadListener() { + final DownloadListener mockDownloadListener = mock(DownloadListener.class); + testInstanceManager.addInstance(mockDownloadListener, 1L); + + testHostApiImpl.setDownloadListener(0L, 1L); + verify(mockWebView).setDownloadListener(mockDownloadListener); + } + + @Test + public void setWebChromeClient() { + final WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + testInstanceManager.addInstance(mockWebChromeClient, 1L); + + testHostApiImpl.setWebChromeClient(0L, 1L); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java new file mode 100644 index 000000000000..31e7d58ee13f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java @@ -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. + +package io.flutter.plugins.webviewflutter.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.junit.Assert; + +public class TestUtils { + public static void setFinalStatic(Class classToModify, String fieldName, Object newValue) { + try { + Field field = classToModify.getField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock static field: " + fieldName); + } + } + + public static void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } + + public static Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/README.md b/packages/webview_flutter/webview_flutter_android/example/README.md index 850ee74397a9..e5bd6e20db63 100644 --- a/packages/webview_flutter/webview_flutter_android/example/README.md +++ b/packages/webview_flutter/webview_flutter_android/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_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle index 1dcd363c9a44..b75bc9df5ebb 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 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.plugins.webviewflutterandroidexample" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -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/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java new file mode 100644 index 000000000000..a63629e0b9e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/BackgroundColorTest.java @@ -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. + +package io.flutter.plugins.webviewflutterexample; + +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static org.junit.Assert.assertEquals; + +import android.graphics.Bitmap; +import android.graphics.Color; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.screenshot.ScreenCapture; +import androidx.test.runner.screenshot.Screenshot; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class BackgroundColorTest { + @Rule + public ActivityTestRule myActivityTestRule = + new ActivityTestRule<>(DriverExtensionActivity.class, true, false); + + @Before + public void setUp() { + ActivityScenario.launch(DriverExtensionActivity.class); + } + + @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748") + @Test + public void backgroundColor() { + onFlutterWidget(withValueKey("ShowPopupMenu")).perform(click()); + onFlutterWidget(withValueKey("ShowTransparentBackgroundExample")).perform(click()); + onFlutterWidget(withText("Transparent background test")); + + final ScreenCapture screenCapture = Screenshot.capture(); + final Bitmap screenBitmap = screenCapture.getBitmap(); + + final int centerLeftColor = + screenBitmap.getPixel(10, (int) Math.floor(screenBitmap.getHeight() / 2.0)); + final int centerColor = + screenBitmap.getPixel( + (int) Math.floor(screenBitmap.getWidth() / 2.0), + (int) Math.floor(screenBitmap.getHeight() / 2.0)); + + // Flutter Colors.green color : 0xFF4CAF50 + // https://github.com/flutter/flutter/blob/f4abaa0735eba4dfd8f33f73363911d63931fe03/packages/flutter/lib/src/material/colors.dart#L1208 + // The background color of the webview is : rgba(0, 0, 0, 0.5) + // The expected color is : rgba(38, 87, 40, 1) -> 0xFF265728 + assertEquals(0xFF265728, centerLeftColor); + assertEquals(Color.RED, centerColor); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml index 28792201bc36..110b9abe1cd8 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml @@ -13,5 +13,13 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> + + diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java similarity index 88% rename from packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java index 9a57f2c1d914..59e1b04c37f2 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/DriverExtensionActivity.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.androidalarmmanagerexample; +package io.flutter.plugins.webviewflutterexample; import androidx.annotation.NonNull; import io.flutter.embedding.android.FlutterActivity; 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 e101ac08df55..e29a4431f2ae 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:3.3.0' + classpath 'com.android.tools.build:gradle:7.1.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 2819f022f1fd..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-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

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

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file 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 e218d908729c..c5bf76d2c6cb 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 @@ -2,14 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + 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/webview_android.dart'; @@ -19,12 +25,31 @@ import 'package:webview_flutter_android_example/navigation_request.dart'; import 'package:webview_flutter_android_example/web_view.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -void main() { +Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); const bool _skipDueToIssue86757 = true; - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; + final String headersUrl = '$prefixUrl/headers'; + testWidgets('initialUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -34,7 +59,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -44,10 +69,10 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }, skip: _skipDueToIssue86757); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -56,7 +81,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -64,12 +89,33 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); + await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + 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(); @@ -80,7 +126,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -98,21 +144,20 @@ void main() { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', - headers: headers); + await controller.loadUrl(headersUrl, headers: headers); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + expect(currentUrl, headersUrl); await pageStarts.stream.firstWhere((String url) => url == currentUrl); await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .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 { + // 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(); @@ -152,100 +197,37 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }, skip: _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, ); - await resizeCompleter.future; + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -286,7 +268,7 @@ void main() { expect(customUserAgent2, 'Custom_User_Agent2'); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // 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 = @@ -298,7 +280,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: _globalKey, - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -403,7 +385,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -432,7 +415,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -463,7 +446,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -491,16 +475,16 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); - Completer pageLoaded = Completer(); - Completer videoPlaying = Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); await tester.pumpWidget( Directionality( @@ -517,7 +501,7 @@ void main() { onMessageReceived: (JavascriptMessage message) { final double currentTime = double.parse(message.message); // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1) { + if (currentTime > 1 && !videoPlaying.isCompleted) { videoPlaying.complete(null); } }, @@ -531,7 +515,7 @@ void main() { ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -540,8 +524,8 @@ void main() { // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); }); @@ -607,7 +591,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -641,7 +626,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -677,7 +662,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -710,13 +696,13 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' + const String getTitleTest = ''' Some title @@ -758,9 +744,9 @@ void main() { }); group('Programmatic Scroll', () { - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -807,7 +793,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); int scrollPosX = await controller.getScrollX(); int scrollPosY = await controller.getScrollY(); @@ -845,9 +831,9 @@ void main() { WebView.platform = AndroidWebView(); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -894,7 +880,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); // Check scrollTo() const int X_SCROLL = 123; @@ -914,10 +900,10 @@ void main() { expect(Y_SCROLL * 2, scrollPosY); }, skip: _skipDueToIssue86757); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets('inputs are scrolled into view when focused', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -969,37 +955,39 @@ void main() { ), ), ); - await Future.delayed(Duration(milliseconds: 20)); + await Future.delayed(const Duration(milliseconds: 20)); await tester.pump(); }); final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - final String viewportRectJSON = await _evaluateJavascript( + final String viewportRectJSON = await _runJavaScriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = - jsonDecode(viewportRectJSON); + jsonDecode(viewportRectJSON) as Map; // Check that the input is originally outside of the viewport. - final String initialInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String initialInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map initialInputClientRectRelativeToViewport = - jsonDecode(initialInputClientRectJSON); + jsonDecode(initialInputClientRectJSON) as Map; expect( initialInputClientRectRelativeToViewport['bottom'] <= viewportRectRelativeToViewport['bottom'], isFalse); - await controller.evaluateJavascript('inputEl.focus()'); + await controller.runJavascript('inputEl.focus()'); // Check that focusing the input brought it into view. - final String lastInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String lastInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map lastInputClientRectRelativeToViewport = - jsonDecode(lastInputClientRectJSON); + jsonDecode(lastInputClientRectJSON) as Map; expect( lastInputClientRectRelativeToViewport['top'] >= @@ -1022,9 +1010,9 @@ void main() { }); group('NavigationDelegate', () { - final String blankPage = ""; - final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + - base64Encode(const Utf8Encoder().convert(blankPage)); + const String 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 = @@ -1053,12 +1041,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -1114,7 +1101,7 @@ void main() { testWidgets( 'onWebResourceError only called for main frame', (WidgetTester tester) async { - final String iframeTest = ''' + const String iframeTest = ''' @@ -1180,7 +1167,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order @@ -1220,12 +1207,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }); }); @@ -1241,7 +1227,7 @@ void main() { height: 300, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1252,7 +1238,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); testWidgets('target _blank opens in same window', @@ -1276,16 +1262,15 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + 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. + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 testWidgets( 'can open new window and go back', (WidgetTester tester) async { @@ -1304,33 +1289,32 @@ void main() { onPageFinished: (String url) { pageLoaded.complete(); }, - initialUrl: 'https://flutter.dev', + initialUrl: primaryUrl, ), ), ); final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller - .evaluateJavascript('window.open("https://www.google.com/")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.currentUrl(), completion(secondaryUrl)); expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); }, skip: _skipDueToIssue86757, ); testWidgets( - 'javascript does not run in parent window', + 'JavaScript does not run in parent window', (WidgetTester tester) async { - final String iframe = ''' + const String iframe = ''' + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } } 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 6176ce255eb9..4492e6e6e26f 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -6,19 +6,29 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; +import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter_android/webview_surface_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'navigation_decision.dart'; +import 'navigation_request.dart'; import 'web_view.dart'; +void appMain() { + enableFlutterDriverExtension(); + main(); +} + void main() { // Configure the [WebView] to use the [SurfaceAndroidWebView] // implementation instead of the default [AndroidWebView]. WebView.platform = SurfaceAndroidWebView(); - runApp(MaterialApp(home: _WebViewExample())); + runApp(const MaterialApp(home: _WebViewExample())); } const String kNavigationExamplePage = ''' @@ -36,6 +46,46 @@ The navigation delegate is set to block navigation to the youtube website. '''; +const String kExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

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

+ + + +'''; + +const String kTransparentBackgroundPage = ''' + + + + Transparent background test + + + +
+

Transparent background test

+
+
+ + +'''; + class _WebViewExample extends StatefulWidget { const _WebViewExample({Key? key}) : super(key: key); @@ -50,6 +100,7 @@ class _WebViewExampleState extends State<_WebViewExample> { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFF4CAF50), appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -58,19 +109,33 @@ 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: (context) { - return WebView( - initialUrl: 'https://flutter.dev', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - javascriptChannels: _createJavascriptChannels(context), - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - ); - }), + body: WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + onProgress: (int progress) { + print('WebView is loading (progress : $progress%)'); + }, + 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'); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + backgroundColor: const Color(0x80000000), + ), floatingActionButton: favoriteButton(), ); } @@ -84,8 +149,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')), ); }, @@ -98,7 +162,7 @@ class _WebViewExampleState extends State<_WebViewExample> { } Set _createJavascriptChannels(BuildContext context) { - return { + return { JavascriptChannel( name: 'Snackbar', onMessageReceived: (JavascriptMessage message) { @@ -116,10 +180,16 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + loadFlutterAsset, + loadLocalFile, + loadHtmlString, + transparentBackground, + doPostRequest, + setCookie, } class _SampleMenu extends StatelessWidget { - _SampleMenu(this.controller); + const _SampleMenu(this.controller); final Future controller; @@ -130,6 +200,7 @@ class _SampleMenu extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot controller) { return PopupMenuButton<_MenuOptions>( + key: const ValueKey('ShowPopupMenu'), onSelected: (_MenuOptions value) { switch (value) { case _MenuOptions.showUserAgent: @@ -153,13 +224,31 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; + case _MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case _MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; + case _MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, 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, @@ -185,26 +274,50 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem<_MenuOptions>( + key: ValueKey('ShowTransparentBackgroundExample'), + value: _MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), ], ); }, ); } - void _onShowUserAgent( + Future _onShowUserAgent( WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Snackbar JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } - void _onListCookies( + Future _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + await controller.runJavascriptReturningResult('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -216,49 +329,86 @@ class _SampleMenu extends StatelessWidget { )); } - void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + Future _onAddToCache( + 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.'), )); } - void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + 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))'); } - void _onClearCache(WebViewController controller, BuildContext context) async { + Future _onClearCache( + WebViewController controller, BuildContext context) async { await controller.clearCache(); - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(const SnackBar( - content: Text("Cache cleared."), + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), )); } - void _onClearCookies( + Future _onClearCookies( WebViewController controller, BuildContext context) async { - final bool hadCookies = await WebView.platform.clearCookies(); + final bool hadCookies = await WebViewCookieManager.instance.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; } - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(message), )); } - void _onNavigationDelegateExample( + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + + Future _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + + Future _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kExamplePage); + } + + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -272,6 +422,21 @@ class _SampleMenu extends StatelessWidget { children: cookieWidgets.toList(), ); } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File('$tmpDir/www/index.html'); + + await Directory('$tmpDir/www').create(recursive: true); + await indexFile.writeAsString(kExamplePage); + + return indexFile.path; + } + + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } } class _NavigationControls extends StatelessWidget { @@ -289,7 +454,7 @@ class _NavigationControls extends StatelessWidget { final bool webViewReady = snapshot.connectionState == ConnectionState.done; final WebViewController? controller = snapshot.data; - if (controller == null) return Container(); + return Row( children: [ IconButton( @@ -297,12 +462,11 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { + if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( - const SnackBar(content: Text("No back history item")), + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), ); return; } @@ -313,13 +477,12 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { + 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")), + content: Text('No forward history item')), ); return; } @@ -330,7 +493,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller!.reload(); }, ), ], @@ -340,5 +503,5 @@ class _NavigationControls extends StatelessWidget { } } -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); +/// Callback type for handling messages sent from JavaScript running in a web view. +typedef JavascriptMessageHandler = void Function(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart index c1ff8dc5a690..6d33126b7c53 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart @@ -14,6 +14,6 @@ class NavigationRequest { @override String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + return '$NavigationRequest(url: $url, isForMainFrame: $isForMainFrame)'; } } 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 33773f96cad8..56745314d92b 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 @@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'navigation_decision.dart'; @@ -15,7 +17,7 @@ import 'navigation_request.dart'; /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); +typedef WebViewCreatedCallback = void Function(WebViewController controller); /// Decides how to handle a specific navigation request. /// @@ -23,20 +25,20 @@ typedef void WebViewCreatedCallback(WebViewController controller); /// `navigation` should be handled. /// /// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( +typedef NavigationDelegate = FutureOr Function( NavigationRequest navigation); /// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); +typedef PageStartedCallback = void Function(String url); /// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); +typedef PageFinishedCallback = void Function(String url); /// Signature for when a [WebView] is loading a page. -typedef void PageLoadingCallback(int progress); +typedef PageLoadingCallback = void Function(int progress); /// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); +typedef WebResourceErrorCallback = void Function(WebResourceError error); /// A web view widget for showing html content. /// @@ -61,6 +63,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const [], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -72,31 +75,20 @@ class WebView extends StatefulWidget { this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, + this.zoomEnabled = true, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), super(key: key); - static WebViewPlatform _platform = AndroidWebView(); - /// The WebView platform that's used by this WebView. /// /// The default value is [AndroidWebView]. - static WebViewPlatform get platform => _platform; - - /// Sets a custom [WebViewPlatform]. - /// - /// This property can be set to use a custom platform implementation for WebViews. - /// - /// Setting `platform` doesn't affect [WebView]s that were already created. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform platform) { - _platform = platform; - } + static WebViewPlatform platform = AndroidWebView(); /// If not null invoked once the web view is created. final WebViewCreatedCallback? onWebViewCreated; @@ -115,7 +107,10 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// The initial cookies to set. + final List initialCookies; + + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -125,7 +120,7 @@ class WebView extends StatefulWidget { /// The JavaScript code can then call `postMessage` on that object to send a message that will be /// passed to [JavascriptChannel.onMessageReceived]. /// - /// For example for the following JavascriptChannel: + /// For example for the following [JavascriptChannel]: /// /// ```dart /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); @@ -188,7 +183,7 @@ class WebView extends StatefulWidget { /// When [onPageFinished] is invoked on Android, the page being rendered may /// not be updated yet. /// - /// When invoked on iOS or Android, any Javascript code that is embedded + /// When invoked on iOS or Android, any JavaScript code that is embedded /// directly in the HTML has been loaded and code injected with /// [WebViewController.evaluateJavascript] can assume this. final PageFinishedCallback? onPageFinished; @@ -221,6 +216,11 @@ class WebView extends StatefulWidget { /// By default `gestureNavigationEnabled` is false. final bool gestureNavigationEnabled; + /// A Boolean value indicating whether the WebView should support zooming using its on-screen zoom controls and gestures. + /// + /// By default 'zoomEnabled' is true + final bool zoomEnabled; + /// The value used for the HTTP User-Agent: request header. /// /// When null the platform's webview default is used for the User-Agent header. @@ -243,8 +243,14 @@ class WebView extends StatefulWidget { /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override - _WebViewState createState() => _WebViewState(); + State createState() => _WebViewState(); } class _WebViewState extends State { @@ -275,7 +281,7 @@ class _WebViewState extends State { context: context, onWebViewPlatformCreated: (WebViewPlatformController? webViewPlatformController) { - WebViewController controller = WebViewController( + final WebViewController controller = WebViewController( widget, webViewPlatformController!, _javascriptChannelRegistry, @@ -294,6 +300,8 @@ class _WebViewState extends State { _javascriptChannelRegistry.channels.keys.toSet(), autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, + backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); @@ -339,6 +347,7 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } } + @override void onWebResourceError(WebResourceError error) { if (_webView.onWebResourceError != null) { _webView.onWebResourceError!(error); @@ -369,6 +378,36 @@ class WebViewController { WebView _widget; + /// 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) { + return _webViewPlatformController.loadFile(absoluteFilePath); + } + + /// 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) { + return _webViewPlatformController.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}) { + return _webViewPlatformController.loadHtmlString( + html, + baseUrl: baseUrl, + ); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will @@ -386,6 +425,11 @@ class WebViewController { return _webViewPlatformController.loadUrl(url, headers); } + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. @@ -481,31 +525,46 @@ class WebViewController { _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); } - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// When running JavaScript in a [WebView], it is best practice to wait for + // the [WebView.onPageFinished] callback. This guarantees all the JavaScript + // embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// 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 on iOS, if the type of the - /// evaluated expression is not supported as described above. + /// Returns the evaluation result as a JSON formatted string. + /// The Future completes with an error if a JavaScript error occurred. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { + Future runJavascriptReturningResult(String javaScriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); } /// Returns the title of the currently loaded page. @@ -553,12 +612,14 @@ class WebViewController { assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); assert(newValue.userAgent != null); + assert(newValue.zoomEnabled != null); JavascriptMode? javascriptMode; bool? hasNavigationDelegate; bool? hasProgressTracking; bool? debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); + WebSetting userAgent = const WebSetting.absent(); + bool? zoomEnabled; if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -574,6 +635,9 @@ class WebViewController { if (currentValue.userAgent != newValue.userAgent) { userAgent = newValue.userAgent; } + if (currentValue.zoomEnabled != newValue.zoomEnabled) { + zoomEnabled = newValue.zoomEnabled; + } return WebSettings( javascriptMode: javascriptMode, @@ -581,6 +645,7 @@ class WebViewController { hasProgressTracking: hasProgressTracking, debuggingEnabled: debuggingEnabled, userAgent: userAgent, + zoomEnabled: zoomEnabled, ); } @@ -613,5 +678,24 @@ WebSettings _webSettingsFromWidget(WebView widget) { gestureNavigationEnabled: widget.gestureNavigationEnabled, allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, userAgent: WebSetting.of(widget.userAgent), + zoomEnabled: widget.zoomEnabled, ); } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_android.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 1e065a6a5b0b..b457ca6b860a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -8,6 +8,9 @@ environment: dependencies: flutter: sdk: flutter + + path_provider: ^2.0.6 + webview_flutter_android: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -17,11 +20,11 @@ dependencies: path: ../ dev_dependencies: - espresso: ^0.1.0+2 - flutter_test: - sdk: flutter + espresso: ^0.2.0 flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter pedantic: ^1.10.0 @@ -31,3 +34,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css 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 new file mode 100644 index 000000000000..7fdcf4b2871f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -0,0 +1,871 @@ +// Copyright 2013 The Flutter 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 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart' show AndroidViewSurface; + +import 'android_webview.pigeon.dart'; +import 'android_webview_api_impls.dart'; + +/// An Android View that displays web pages. +/// +/// **Basic usage** +/// In most cases, we recommend using a standard web browser, like Chrome, to +/// deliver content to the user. To learn more about web browsers, read the +/// guide on invoking a browser with +/// [url_launcher](https://pub.dev/packages/url_launcher). +/// +/// WebView objects allow you to display web content as part of your widget +/// layout, but lack some of the features of fully-developed browsers. A WebView +/// is useful when you need increased control over the UI and advanced +/// configuration options that will allow you to embed web pages in a +/// specially-designed environment for your app. +/// +/// To learn more about WebView and alternatives for serving web content, read +/// the documentation on +/// [Web-based content](https://developer.android.com/guide/webapps). +/// +/// When a [WebView] is no longer needed [release] must be called. +class WebView { + /// Constructs a new WebView. + WebView({this.useHybridComposition = false}) { + api.createFromInstance(this); + } + + /// Pigeon Host Api implementation for [WebView]. + @visibleForTesting + static WebViewHostApiImpl api = WebViewHostApiImpl(); + + WebViewClient? _currentWebViewClient; + + /// Whether the [WebView] will be rendered with an [AndroidViewSurface]. + /// + /// This implementation uses hybrid composition to render the WebView Widget. + /// This comes at the cost of some performance on Android versions below 10. + /// See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance + /// for more information. + /// + /// Defaults to false. + final bool useHybridComposition; + + /// The [WebSettings] object used to control the settings for this WebView. + late final WebSettings settings = WebSettings(this); + + /// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. + /// + /// This flag can be enabled in order to facilitate debugging of web layouts + /// and JavaScript code running inside WebViews. Please refer to [WebView] + /// documentation for the debugging guide. The default is false. + static Future setWebContentsDebuggingEnabled(bool enabled) { + return api.setWebContentsDebuggingEnabled(enabled); + } + + /// Loads the given data into this WebView using a 'data' scheme URL. + /// + /// Note that JavaScript's same origin policy means that script running in a + /// page loaded using this method will be unable to access content loaded + /// using any scheme other than 'data', including 'http(s)'. To avoid this + /// restriction, use [loadDataWithBaseURL()] with an appropriate base URL. + /// + /// The [encoding] parameter specifies whether the data is base64 or URL + /// encoded. If the data is base64 encoded, the value of the encoding + /// parameter must be `'base64'`. HTML can be encoded with + /// `base64.encode(bytes)` like so: + /// ```dart + /// import 'dart:convert'; + /// + /// final unencodedHtml = ''' + /// '%28' is the code for '(' + /// '''; + /// final encodedHtml = base64.encode(utf8.encode(unencodedHtml)); + /// print(encodedHtml); + /// ``` + /// + /// The [mimeType] parameter specifies the format of the data. If WebView + /// can't handle the specified MIME type, it will download the data. If + /// `null`, defaults to 'text/html'. + Future loadData({ + required String data, + String? mimeType, + String? encoding, + }) { + return api.loadDataFromInstance( + this, + data, + mimeType, + encoding, + ); + } + + /// Loads the given data into this WebView. + /// + /// The [baseUrl] is used as base URL for the content. It is used both to + /// resolve relative URLs and when applying JavaScript's same origin policy. + /// + /// The [historyUrl] is used for the history entry. + /// + /// The [mimeType] parameter specifies the format of the data. If WebView + /// can't handle the specified MIME type, it will download the data. If + /// `null`, defaults to 'text/html'. + /// + /// Note that content specified in this way can access local device files (via + /// 'file' scheme URLs) only if baseUrl specifies a scheme other than 'http', + /// 'https', 'ftp', 'ftps', 'about' or 'javascript'. + /// + /// If the base URL uses the data scheme, this method is equivalent to calling + /// [loadData] and the [historyUrl] is ignored, and the data will be treated + /// as part of a data: URL, including the requirement that the content be + /// URL-encoded or base64 encoded. If the base URL uses any other scheme, then + /// the data will be loaded into the WebView as a plain string (i.e. not part + /// of a data URL) and any URL-encoded entities in the string will not be + /// decoded. + /// + /// Note that the [baseUrl] is sent in the 'Referer' HTTP header when + /// requesting subresources (images, etc.) of the page loaded using this + /// method. + /// + /// If a valid HTTP or HTTPS base URL is not specified in [baseUrl], then + /// content loaded using this method will have a `window.origin` value of + /// `"null"`. This must not be considered to be a trusted origin by the + /// application or by any JavaScript code running inside the WebView (for + /// example, event sources in DOM event handlers or web messages), because + /// malicious content can also create frames with a null origin. If you need + /// to identify the main frame's origin in a trustworthy way, you should use a + /// valid HTTP or HTTPS base URL to set the origin. + Future loadDataWithBaseUrl({ + String? baseUrl, + required String data, + String? mimeType, + String? encoding, + String? historyUrl, + }) { + return api.loadDataWithBaseUrlFromInstance( + this, + baseUrl, + data, + mimeType, + encoding, + historyUrl, + ); + } + + /// Loads the given URL with additional HTTP headers, specified as a map from name to value. + /// + /// Note that if this map contains any of the headers that are set by default + /// by this WebView, such as those controlling caching, accept types or the + /// User-Agent, their values may be overridden by this WebView's defaults. + /// + /// Also see compatibility note on [evaluateJavascript]. + Future loadUrl(String url, Map headers) { + return api.loadUrlFromInstance(this, url, headers); + } + + /// Loads the URL with postData using "POST" method into this WebView. + /// + /// If url is not a network URL, it will be loaded with [loadUrl] instead, ignoring the postData param. + Future postUrl(String url, Uint8List data) { + return api.postUrlFromInstance(this, url, data); + } + + /// Gets the URL for the current page. + /// + /// This is not always the same as the URL passed to + /// [WebViewClient.onPageStarted] because although the load for that URL has + /// begun, the current page may not have changed. + /// + /// Returns null if no page has been loaded. + Future getUrl() { + return api.getUrlFromInstance(this); + } + + /// Whether this WebView has a back history item. + Future canGoBack() { + return api.canGoBackFromInstance(this); + } + + /// Whether this WebView has a forward history item. + Future canGoForward() { + return api.canGoForwardFromInstance(this); + } + + /// Goes back in the history of this WebView. + Future goBack() { + return api.goBackFromInstance(this); + } + + /// Goes forward in the history of this WebView. + Future goForward() { + return api.goForwardFromInstance(this); + } + + /// Reloads the current URL. + Future reload() { + return api.reloadFromInstance(this); + } + + /// Clears the resource cache. + /// + /// Note that the cache is per-application, so this will clear the cache for + /// all WebViews used. + Future clearCache(bool includeDiskFiles) { + return api.clearCacheFromInstance(this, includeDiskFiles); + } + + // TODO(bparrishMines): Update documentation once addJavascriptInterface is added. + /// Asynchronously evaluates JavaScript in the context of the currently displayed page. + /// + /// If non-null, the returned value will be any result returned from that + /// execution. + /// + /// Compatibility note. Applications targeting Android versions N or later, + /// 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) { + return api.evaluateJavascriptFromInstance( + this, + javascriptString, + ); + } + + // 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() { + return api.getTitleFromInstance(this); + } + + // TODO(bparrishMines): Update documentation when onScrollChanged is added. + /// Set the scrolled position of your view. + Future scrollTo(int x, int y) { + return api.scrollToFromInstance(this, x, y); + } + + // TODO(bparrishMines): Update documentation when onScrollChanged is added. + /// Move the scrolled position of your view. + Future scrollBy(int x, int y) { + return api.scrollByFromInstance(this, x, y); + } + + /// Return the scrolled left position of this view. + /// + /// This is the left edge of the displayed part of your view. You do not + /// need to draw any pixels farther left, since those are outside of the frame + /// of your view on screen. + Future getScrollX() { + return api.getScrollXFromInstance(this); + } + + /// Return the scrolled top position of this view. + /// + /// This is the top edge of the displayed part of your view. You do not need + /// to draw any pixels above it, since those are outside of the frame of your + /// view on screen. + Future getScrollY() { + return api.getScrollYFromInstance(this); + } + + /// Sets the [WebViewClient] that will receive various notifications and requests. + /// + /// This will replace the current handler. + Future setWebViewClient(WebViewClient webViewClient) { + _currentWebViewClient = webViewClient; + WebViewClient.api.createFromInstance(webViewClient); + return api.setWebViewClientFromInstance(this, webViewClient); + } + + /// Injects the supplied [JavascriptChannel] into this WebView. + /// + /// The object is injected into all frames of the web page, including all the + /// iframes, using the supplied name. This allows the object's methods to + /// be accessed from JavaScript. + /// + /// Note that injected objects will not appear in JavaScript until the page is + /// next (re)loaded. JavaScript should be enabled before injecting the object. + /// For example: + /// + /// ```dart + /// webview.settings.setJavaScriptEnabled(true); + /// webView.addJavascriptChannel(JavScriptChannel("injectedObject")); + /// webView.loadUrl("about:blank", {}); + /// webView.loadUrl("javascript:injectedObject.postMessage("Hello, World!")", {}); + /// ``` + /// + /// **Important** + /// * Because the object is exposed to all the frames, any frame could obtain + /// the object name and call methods on it. There is no way to tell the + /// calling frame's origin from the app side, so the app must not assume that + /// the caller is trustworthy unless the app can guarantee that no third party + /// content is ever loaded into the WebView even inside an iframe. + Future addJavaScriptChannel(JavaScriptChannel javaScriptChannel) { + JavaScriptChannel.api.createFromInstance(javaScriptChannel); + return api.addJavaScriptChannelFromInstance(this, javaScriptChannel); + } + + /// Removes a previously injected [JavaScriptChannel] from this WebView. + /// + /// Note that the removal will not be reflected in JavaScript until the page + /// is next (re)loaded. See [addJavaScriptChannel]. + Future removeJavaScriptChannel(JavaScriptChannel javaScriptChannel) { + JavaScriptChannel.api.createFromInstance(javaScriptChannel); + return api.removeJavaScriptChannelFromInstance(this, javaScriptChannel); + } + + /// 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) async { + await Future.wait(>[ + if (listener != null) DownloadListener.api.createFromInstance(listener), + api.setDownloadListenerFromInstance(this, listener) + ]); + } + + /// Sets the chrome handler. + /// + /// 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) 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 + // a secure url. + assert( + _currentWebViewClient != null, + "Can't set a WebChromeClient without setting a WebViewClient first.", + ); + await Future.wait(>[ + if (client != null) + WebChromeClient.api.createFromInstance(client, _currentWebViewClient!), + api.setWebChromeClientFromInstance(this, client), + ]); + } + + /// Sets the background color of this WebView. + Future setBackgroundColor(Color color) { + return api.setBackgroundColorFromInstance(this, color.value); + } + + /// Releases all resources used by the [WebView]. + /// + /// Any methods called after [release] will throw an exception. + Future release() { + _currentWebViewClient = null; + WebSettings.api.disposeFromInstance(settings); + return api.disposeFromInstance(this); + } +} + +/// Manages cookies globally for all webviews. +class CookieManager { + CookieManager._(); + + static CookieManager? _instance; + + /// Gets the globally set CookieManager instance. + static CookieManager get instance => _instance ??= CookieManager._(); + + /// Setter for the singleton value, for testing purposes only. + @visibleForTesting + static set instance(CookieManager value) => _instance = value; + + /// Pigeon Host Api implementation for [CookieManager]. + @visibleForTesting + static CookieManagerHostApi api = CookieManagerHostApi(); + + /// Sets a single cookie (key-value pair) for the given URL. Any existing + /// cookie with the same host, path and name will be replaced with the new + /// cookie. The cookie being set will be ignored if it is expired. To set + /// multiple cookies, your application should invoke this method multiple + /// times. + /// + /// The value parameter must follow the format of the Set-Cookie HTTP + /// response header defined by RFC6265bis. This is a key-value pair of the + /// form "key=value", optionally followed by a list of cookie attributes + /// delimited with semicolons (ex. "key=value; Max-Age=123"). Please consult + /// the RFC specification for a list of valid attributes. + /// + /// Note: if specifying a value containing the "Secure" attribute, url must + /// use the "https://" scheme. + /// + /// Params: + /// url – the URL for which the cookie is to be set + /// value – the cookie as a string, using the format of the 'Set-Cookie' HTTP response header + Future setCookie(String url, String value) => api.setCookie(url, value); + + /// Removes all cookies. + /// + /// The returned future resolves to true if any cookies were removed. + Future clearCookies() => api.clearCookies(); +} + +/// Manages settings state for a [WebView]. +/// +/// When a WebView is first created, it obtains a set of default settings. These +/// default settings will be returned from any getter call. A WebSettings object +/// 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 { + /// Constructs a [WebSettings]. + /// + /// This constructor is only used for testing. An instance should be obtained + /// with [WebView.settings]. + @visibleForTesting + WebSettings(WebView webView) { + api.createFromInstance(this, webView); + } + + /// Pigeon Host Api implementation for [WebSettings]. + @visibleForTesting + static WebSettingsHostApiImpl api = WebSettingsHostApiImpl(); + + /// Sets whether the DOM storage API is enabled. + /// + /// The default value is false. + Future setDomStorageEnabled(bool flag) { + return api.setDomStorageEnabledFromInstance(this, flag); + } + + /// Tells JavaScript to open windows automatically. + /// + /// This applies to the JavaScript function `window.open()`. The default is + /// false. + Future setJavaScriptCanOpenWindowsAutomatically(bool flag) { + return api.setJavaScriptCanOpenWindowsAutomaticallyFromInstance( + this, + flag, + ); + } + + // TODO(bparrishMines): Update documentation when WebChromeClient.onCreateWindow is added. + /// Sets whether the WebView should supports multiple windows. + /// + /// The default is false. + Future setSupportMultipleWindows(bool support) { + return api.setSupportMultipleWindowsFromInstance(this, support); + } + + /// Tells the WebView to enable JavaScript execution. + /// + /// The default is false. + Future setJavaScriptEnabled(bool flag) { + return api.setJavaScriptEnabledFromInstance(this, flag); + } + + /// Sets the WebView's user-agent string. + /// + /// 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) { + return api.setUserAgentStringFromInstance(this, userAgentString); + } + + /// Sets whether the WebView requires a user gesture to play media. + /// + /// The default is true. + Future setMediaPlaybackRequiresUserGesture(bool require) { + return api.setMediaPlaybackRequiresUserGestureFromInstance(this, require); + } + + // TODO(bparrishMines): Update documentation when WebView.zoomIn and WebView.zoomOut are added. + /// Sets whether the WebView should support zooming using its on-screen zoom controls and gestures. + /// + /// The particular zoom mechanisms that should be used can be set with + /// [setBuiltInZoomControls]. + /// + /// The default is true. + Future setSupportZoom(bool support) { + return api.setSupportZoomFromInstance(this, support); + } + + /// Sets whether the WebView loads pages in overview mode, that is, zooms out the content to fit on screen by width. + /// + /// This setting is taken into account when the content width is greater than + /// the width of the WebView control, for example, when [setUseWideViewPort] + /// is enabled. + /// + /// The default is false. + Future setLoadWithOverviewMode(bool overview) { + return api.setLoadWithOverviewModeFromInstance(this, overview); + } + + /// Sets whether the WebView should enable support for the "viewport" HTML meta tag or should use a wide viewport. + /// + /// When the value of the setting is false, the layout width is always set to + /// the width of the WebView control in device-independent (CSS) pixels. When + /// the value is true and the page contains the viewport meta tag, the value + /// of the width specified in the tag is used. If the page does not contain + /// the tag or does not provide a width, then a wide viewport will be used. + Future setUseWideViewPort(bool use) { + return api.setUseWideViewPortFromInstance(this, use); + } + + // TODO(bparrishMines): Update documentation when ZoomButtonsController is added. + /// Sets whether the WebView should display on-screen zoom controls when using the built-in zoom mechanisms. + /// + /// See [setBuiltInZoomControls]. The default is true. However, on-screen zoom + /// controls are deprecated in Android so it's recommended to set this to + /// false. + Future setDisplayZoomControls(bool enabled) { + return api.setDisplayZoomControlsFromInstance(this, enabled); + } + + // TODO(bparrishMines): Update documentation when ZoomButtonsController is added. + /// Sets whether the WebView should use its built-in zoom mechanisms. + /// + /// The built-in zoom mechanisms comprise on-screen zoom controls, which are + /// displayed over the WebView's content, and the use of a pinch gesture to + /// control zooming. Whether or not these on-screen controls are displayed can + /// be set with [setDisplayZoomControls]. The default is false. + /// + /// The built-in mechanisms are the only currently supported zoom mechanisms, + /// so it is recommended that this setting is always enabled. However, + /// on-screen zoom controls are deprecated in Android so it's recommended to + /// disable [setDisplayZoomControls]. + Future setBuiltInZoomControls(bool enabled) { + return api.setBuiltInZoomControlsFromInstance(this, enabled); + } + + /// Enables or disables file access within WebView. + /// + /// This enables or disables file system access only. Assets and resources are + /// still accessible using file:///android_asset and file:///android_res. The + /// default value is true for apps targeting Build.VERSION_CODES.Q and below, + /// and false when targeting Build.VERSION_CODES.R and above. + Future setAllowFileAccess(bool enabled) { + return api.setAllowFileAccessFromInstance(this, enabled); + } +} + +/// Exposes a channel to receive calls from javaScript. +/// +/// See [WebView.addJavaScriptChannel]. +abstract class JavaScriptChannel { + /// Constructs a [JavaScriptChannel]. + JavaScriptChannel(this.channelName) { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } + + /// Pigeon Host Api implementation for [JavaScriptChannel]. + @visibleForTesting + static JavaScriptChannelHostApiImpl api = JavaScriptChannelHostApiImpl(); + + /// Used to identify this object to receive messages from javaScript. + final String channelName; + + /// Callback method when javaScript calls `postMessage` on the object instance passed. + void postMessage(String message); +} + +/// Receive various notifications and requests for [WebView]. +abstract class WebViewClient { + /// Constructs a [WebViewClient]. + WebViewClient({this.shouldOverrideUrlLoading = true}) { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } + + /// User authentication failed on server. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_AUTHENTICATION + static const int errorAuthentication = -4; + + /// Malformed URL. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_BAD_URL + static const int errorBadUrl = -12; + + /// Failed to connect to the server. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_CONNECT + static const int errorConnect = -6; + + /// Failed to perform SSL handshake. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_FAILED_SSL_HANDSHAKE + static const int errorFailedSslHandshake = -11; + + /// Generic file error. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_FILE + static const int errorFile = -13; + + /// File not found. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_FILE_NOT_FOUND + static const int errorFileNotFound = -14; + + /// Server or proxy hostname lookup failed. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_HOST_LOOKUP + static const int errorHostLookup = -2; + + /// Failed to read or write to the server. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_IO + static const int errorIO = -7; + + /// User authentication failed on proxy. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_PROXY_AUTHENTICATION + static const int errorProxyAuthentication = -5; + + /// Too many redirects. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_REDIRECT_LOOP + static const int errorRedirectLoop = -9; + + /// Connection timed out. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_TIMEOUT + static const int errorTimeout = -8; + + /// Too many requests during this load. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_TOO_MANY_REQUESTS + static const int errorTooManyRequests = -15; + + /// Generic error. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNKNOWN + static const int errorUnknown = -1; + + /// Resource load was canceled by Safe Browsing. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSAFE_RESOURCE + static const int errorUnsafeResource = -16; + + /// Unsupported authentication scheme (not basic or digest). + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSUPPORTED_AUTH_SCHEME + static const int errorUnsupportedAuthScheme = -3; + + /// Unsupported URI scheme. + /// + /// See https://developer.android.com/reference/android/webkit/WebViewClient#ERROR_UNSUPPORTED_SCHEME + static const int errorUnsupportedScheme = -10; + + /// Pigeon Host Api implementation for [WebViewClient]. + @visibleForTesting + static WebViewClientHostApiImpl api = WebViewClientHostApiImpl(); + + /// Whether loading a url should be overridden. + /// + /// In Java, `shouldOverrideUrlLoading()` and `shouldOverrideRequestLoading()` + /// callbacks must synchronously return a boolean. This sets the default + /// return value. + /// + /// Setting [shouldOverrideUrlLoading] to true causes the current [WebView] to + /// abort loading the URL, while returning false causes the [WebView] to + /// continue loading the URL as usual. [requestLoading] or [urlLoading] will + /// still be called either way. + /// + /// Defaults to true. + final bool shouldOverrideUrlLoading; + + /// Notify the host application that a page has started loading. + /// + /// This method is called once for each main frame load so a page with iframes + /// or framesets will call onPageStarted one time for the main frame. This + /// also means that [onPageStarted] will not be called when the contents of an + /// embedded frame changes, i.e. clicking a link whose target is an iframe, it + /// will also not be called for fragment navigations (navigations to + /// #fragment_id). + void onPageStarted(WebView webView, String url) {} + + // TODO(bparrishMines): Update documentation when WebView.postVisualStateCallback is added. + /// Notify the host application that a page has finished loading. + /// + /// This method is called only for main frame. Receiving an [onPageFinished] + /// callback does not guarantee that the next frame drawn by WebView will + /// reflect the state of the DOM at this point. + void onPageFinished(WebView webView, String url) {} + + /// Report web resource loading error to the host application. + /// + /// These errors usually indicate inability to connect to the server. Note + /// that unlike the deprecated version of the callback, the new version will + /// be called for any resource (iframe, image, etc.), not just for the main + /// page. Thus, it is recommended to perform minimum required work in this + /// callback. + void onReceivedRequestError( + WebView webView, + WebResourceRequest request, + WebResourceError error, + ) {} + + /// Report an error to the host application. + /// + /// These errors are unrecoverable (i.e. the main resource is unavailable). + /// The errorCode parameter corresponds to one of the error* constants. + @Deprecated('Only called on Android version < 23.') + void onReceivedError( + WebView webView, + int errorCode, + String description, + String failingUrl, + ) {} + + // TODO(bparrishMines): Update documentation once synchronous url handling is supported. + /// When a URL is about to be loaded in the current [WebView]. + /// + /// If a [WebViewClient] is not provided, by default [WebView] will ask + /// Activity Manager to choose the proper handler for the URL. If a + /// [WebViewClient] is provided, setting [shouldOverrideUrlLoading] to true + /// causes the current [WebView] to abort loading the URL, while returning + /// false causes the [WebView] to continue loading the URL as usual. + void requestLoading(WebView webView, WebResourceRequest request) {} + + // TODO(bparrishMines): Update documentation once synchronous url handling is supported. + /// When a URL is about to be loaded in the current [WebView]. + /// + /// If a [WebViewClient] is not provided, by default [WebView] will ask + /// Activity Manager to choose the proper handler for the URL. If a + /// [WebViewClient] is provided, setting [shouldOverrideUrlLoading] to true + /// 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) {} +} + +/// 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 { + /// Constructs a [DownloadListener]. + DownloadListener() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } + + /// Pigeon Host Api implementation for [DownloadListener]. + @visibleForTesting + static DownloadListenerHostApiImpl api = DownloadListenerHostApiImpl(); + + /// Notify the host application that a file should be downloaded. + void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ); +} + +/// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. +abstract class WebChromeClient { + /// Constructs a [WebChromeClient]. + WebChromeClient() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + } + + /// 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) {} +} + +/// Encompasses parameters to the [WebViewClient.requestLoading] method. +class WebResourceRequest { + /// Constructs a [WebResourceRequest]. + WebResourceRequest({ + required this.url, + required this.isForMainFrame, + required this.isRedirect, + required this.hasGesture, + required this.method, + required this.requestHeaders, + }); + + /// The URL for which the resource request was made. + final String url; + + /// Whether the request was made in order to fetch the main frame's document. + final bool isForMainFrame; + + /// Whether the request was a result of a server-side redirect. + /// + /// Only supported on Android version >= 24. + final bool? isRedirect; + + /// Whether a gesture (such as a click) was associated with the request. + final bool hasGesture; + + /// The method associated with the request, for example "GET". + final String method; + + /// The headers associated with the request. + final Map requestHeaders; +} + +/// Encapsulates information about errors occurred during loading of web resources. +/// +/// See [WebViewClient.onReceivedRequestError]. +class WebResourceError { + /// Constructs a [WebResourceError]. + WebResourceError({ + required this.errorCode, + required this.description, + }); + + /// The integer code of the error (e.g. [WebViewClient.errorAuthentication]. + final int errorCode; + + /// Describes the error. + final String description; +} + +/// Manages Flutter assets that are part of Android's app bundle. +class FlutterAssetManager { + /// Constructs the [FlutterAssetManager]. + const FlutterAssetManager(); + + /// Pigeon Host Api implementation for [FlutterAssetManager]. + @visibleForTesting + static FlutterAssetManagerHostApi api = FlutterAssetManagerHostApi(); + + /// Lists all assets at the given path. + /// + /// The assets are returned as a `List`. The `List` only + /// contains files which are direct childs + Future> list(String path) => api.list(path); + + /// Gets the relative file path to the Flutter asset with the given name. + 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 { + /// Constructs a [WebStorage]. + /// + /// This constructor is only used for testing. An instance should be obtained + /// with [WebStorage.instance]. + @visibleForTesting + WebStorage() { + AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); + } + + /// 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); + } +} 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 new file mode 100644 index 000000000000..4491e162ce9c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -0,0 +1,1931 @@ +// Copyright 2013 The Flutter 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'; + +class WebResourceRequestData { + 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; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['url'] = url; + pigeonMap['isForMainFrame'] = isForMainFrame; + pigeonMap['isRedirect'] = isRedirect; + pigeonMap['hasGesture'] = hasGesture; + pigeonMap['method'] = method; + pigeonMap['requestHeaders'] = requestHeaders; + return pigeonMap; + } + + static WebResourceRequestData decode(Object message) { + final Map pigeonMap = message as Map; + return 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 { + WebResourceErrorData({ + required this.errorCode, + required this.description, + }); + + int errorCode; + String description; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['errorCode'] = errorCode; + pigeonMap['description'] = description; + return pigeonMap; + } + + static WebResourceErrorData decode(Object message) { + final Map pigeonMap = message as Map; + return WebResourceErrorData( + errorCode: pigeonMap['errorCode']! as int, + description: pigeonMap['description']! as String, + ); + } +} + +class _CookieManagerHostApiCodec extends StandardMessageCodec { + const _CookieManagerHostApiCodec(); +} + +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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CookieManagerHostApiCodec(); + + Future clearCookies() async { + final BasicMessageChannel channel = BasicMessageChannel( + '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.', + ); + } 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 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?; + 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 _WebViewHostApiCodec extends StandardMessageCodec { + const _WebViewHostApiCodec(); +} + +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; + + 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); + final Map? replyMap = + await channel.send([arg_instanceId, arg_useHybridComposition]) + 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 dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.dispose', 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 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?; + 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 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?; + 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 loadUrl(int arg_instanceId, String arg_url, + Map arg_headers) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + 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.', + ); + } 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 postUrl( + int arg_instanceId, String arg_url, Uint8List arg_data) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_url, arg_data]) + as Map?; + if (replyMap == null) { + 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_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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 canGoBack(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.reload', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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 clearCache(int arg_instanceId, bool arg_includeDiskFiles) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_includeDiskFiles]) + 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_instanceId, String arg_javascriptString) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, 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 String?); + } + } + + Future getTitle(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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 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?; + 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 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?; + 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 getScrollX(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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 getScrollY(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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 setWebContentsDebuggingEnabled(bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([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; + } + } + + 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?; + 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 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?; + 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 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?; + 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 setDownloadListener( + int arg_instanceId, int? arg_listenerInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_listenerInstanceId]) + 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 setWebChromeClient( + int arg_instanceId, int? arg_clientInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_clientInstanceId]) + 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 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?; + 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 _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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebSettingsHostApiCodec(); + + Future create(int arg_instanceId, int arg_webViewInstanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_webViewInstanceId]) + 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 dispose(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.dispose', 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 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?; + 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 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?; + 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 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?; + 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_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?; + 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 setUserAgentString( + int arg_instanceId, String? arg_userAgentString) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_userAgentString]) + as Map?; + if (replyMap == null) { + 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 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?; + 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 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?; + 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 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?; + 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 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?; + 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 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?; + 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 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?; + 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 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?; + 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 _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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _JavaScriptChannelHostApiCodec(); + + Future create(int arg_instanceId, String arg_channelName) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId, arg_channelName]) + as Map?; + if (replyMap == null) { + 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 _JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { + const _JavaScriptChannelFlutterApiCodec(); +} + +abstract class JavaScriptChannelFlutterApi { + static const MessageCodec codec = + _JavaScriptChannelFlutterApiCodec(); + + void dispose(int instanceId); + void postMessage(int instanceId, String message); + static void setup(JavaScriptChannelFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.dispose(arg_instanceId!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.postMessage(arg_instanceId!, arg_message!); + return; + }); + } + } + } +} + +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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebViewClientHostApiCodec(); + + 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?; + 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 _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) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return WebResourceErrorData.decode(readValue(buffer)!); + + case 129: + return WebResourceRequestData.decode(readValue(buffer)!); + + 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 urlLoading(int instanceId, int webViewInstanceId, String url); + static void setup(WebViewClientFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.dispose(arg_instanceId!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + api.onPageStarted(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + api.onPageFinished(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + 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.'); + 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!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + api.urlLoading(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } + } +} + +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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _DownloadListenerHostApiCodec(); + + Future create(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_instanceId]) as Map?; + if (replyMap == null) { + 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 _DownloadListenerFlutterApiCodec extends StandardMessageCodec { + const _DownloadListenerFlutterApiCodec(); +} + +abstract class DownloadListenerFlutterApi { + static const MessageCodec codec = _DownloadListenerFlutterApiCodec(); + + 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}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.dispose(arg_instanceId!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + 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.'); + 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.'); + 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!); + return; + }); + } + } + } +} + +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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _WebChromeClientHostApiCodec(); + + 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?; + 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 _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; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FlutterAssetManagerHostApiCodec(); + + Future> list(String arg_path) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_path]) as Map?; + if (replyMap == null) { + 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 getAssetFilePathByName(String arg_name) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([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 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(); + + void dispose(int instanceId); + void onProgressChanged(int instanceId, int webViewInstanceId, int progress); + static void setup(WebChromeClientFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.dispose(arg_instanceId!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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!); + return; + }); + } + } + } +} + +class _WebStorageHostApiCodec extends StandardMessageCodec { + const _WebStorageHostApiCodec(); +} + +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 = _WebStorageHostApiCodec(); + + 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 new file mode 100644 index 000000000000..b1268f4f9cb0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -0,0 +1,819 @@ +// Copyright 2013 The Flutter 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#106316) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +import 'android_webview.dart'; +import 'android_webview.pigeon.dart'; +import 'instance_manager.dart'; + +/// Converts [WebResourceRequestData] to [WebResourceRequest] +WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { + return WebResourceRequest( + url: data.url, + isForMainFrame: data.isForMainFrame, + isRedirect: data.isRedirect, + 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, + ); +} + +/// Handles initialization of Flutter APIs for Android WebView. +class AndroidWebViewFlutterApis { + /// Creates a [AndroidWebViewFlutterApis]. + AndroidWebViewFlutterApis({ + DownloadListenerFlutterApiImpl? downloadListenerFlutterApi, + WebViewClientFlutterApiImpl? webViewClientFlutterApi, + WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, + JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, + }) { + this.downloadListenerFlutterApi = + downloadListenerFlutterApi ?? DownloadListenerFlutterApiImpl(); + this.webViewClientFlutterApi = + webViewClientFlutterApi ?? WebViewClientFlutterApiImpl(); + this.webChromeClientFlutterApi = + webChromeClientFlutterApi ?? WebChromeClientFlutterApiImpl(); + this.javaScriptChannelFlutterApi = + javaScriptChannelFlutterApi ?? JavaScriptChannelFlutterApiImpl(); + } + + static bool _haveBeenSetUp = false; + + /// Mutable instance containing all Flutter Apis for Android WebView. + /// + /// This should only be changed for testing purposes. + static AndroidWebViewFlutterApis instance = AndroidWebViewFlutterApis(); + + /// Flutter Api for [DownloadListener]. + late final DownloadListenerFlutterApiImpl downloadListenerFlutterApi; + + /// Flutter Api for [WebViewClient]. + late final WebViewClientFlutterApiImpl webViewClientFlutterApi; + + /// Flutter Api for [WebChromeClient]. + late final WebChromeClientFlutterApiImpl webChromeClientFlutterApi; + + /// Flutter Api for [JavaScriptChannel]. + late final JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi; + + /// Ensures all the Flutter APIs have been setup to receive calls from native code. + void ensureSetUp() { + if (!_haveBeenSetUp) { + DownloadListenerFlutterApi.setup(downloadListenerFlutterApi); + WebViewClientFlutterApi.setup(webViewClientFlutterApi); + WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); + JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); + _haveBeenSetUp = true; + } + } +} + +/// 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; + } + + /// Maintains instances stored to communicate with java objects. + late 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); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(WebView instance) async { + final int? instanceId = instanceManager.getInstanceId(instance); + if (instanceId != null) { + 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, + ) { + return loadData( + instanceManager.getInstanceId(instance)!, + data, + mimeType, + encoding, + ); + } + + /// Helper method to convert instances ids to objects. + Future loadDataWithBaseUrlFromInstance( + WebView instance, + String? baseUrl, + String data, + String? mimeType, + String? encoding, + String? historyUrl, + ) { + return loadDataWithBaseUrl( + instanceManager.getInstanceId(instance)!, + baseUrl, + data, + mimeType, + encoding, + historyUrl, + ); + } + + /// Helper method to convert instances ids to objects. + Future loadUrlFromInstance( + WebView instance, + String url, + Map headers, + ) { + return loadUrl(instanceManager.getInstanceId(instance)!, url, headers); + } + + /// Helper method to convert instances ids to objects. + Future postUrlFromInstance( + WebView instance, + String url, + Uint8List data, + ) { + return postUrl(instanceManager.getInstanceId(instance)!, url, data); + } + + /// Helper method to convert instances ids to objects. + Future getUrlFromInstance(WebView instance) { + return getUrl(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future canGoBackFromInstance(WebView instance) { + return canGoBack(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future canGoForwardFromInstance(WebView instance) { + return canGoForward(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future goBackFromInstance(WebView instance) { + return goBack(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future goForwardFromInstance(WebView instance) { + return goForward(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future reloadFromInstance(WebView instance) { + return reload(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future clearCacheFromInstance(WebView instance, bool includeDiskFiles) { + return clearCache( + instanceManager.getInstanceId(instance)!, + includeDiskFiles, + ); + } + + /// Helper method to convert instances ids to objects. + Future evaluateJavascriptFromInstance( + WebView instance, + String javascriptString, + ) { + return evaluateJavascript( + instanceManager.getInstanceId(instance)!, + javascriptString, + ); + } + + /// Helper method to convert instances ids to objects. + Future getTitleFromInstance(WebView instance) { + return getTitle(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future scrollToFromInstance(WebView instance, int x, int y) { + return scrollTo(instanceManager.getInstanceId(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); + } + + /// Helper method to convert instances ids to objects. + Future getScrollXFromInstance(WebView instance) { + return getScrollX(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future getScrollYFromInstance(WebView instance) { + return getScrollY(instanceManager.getInstanceId(instance)!); + } + + /// Helper method to convert instances ids to objects. + Future setWebViewClientFromInstance( + WebView instance, + WebViewClient webViewClient, + ) { + return setWebViewClient( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(webViewClient)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future addJavaScriptChannelFromInstance( + WebView instance, + JavaScriptChannel javaScriptChannel, + ) { + return addJavaScriptChannel( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(javaScriptChannel)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future removeJavaScriptChannelFromInstance( + WebView instance, + JavaScriptChannel javaScriptChannel, + ) { + return removeJavaScriptChannel( + instanceManager.getInstanceId(instance)!, + instanceManager.getInstanceId(javaScriptChannel)!, + ); + } + + /// Helper method to convert instances ids to objects. + Future setDownloadListenerFromInstance( + WebView instance, + DownloadListener? listener, + ) { + return setDownloadListener( + instanceManager.getInstanceId(instance)!, + listener != null ? instanceManager.getInstanceId(listener) : null, + ); + } + + /// Helper method to convert instances ids to objects. + Future setWebChromeClientFromInstance( + WebView instance, + WebChromeClient? client, + ) { + return setWebChromeClient( + instanceManager.getInstanceId(instance)!, + client != null ? instanceManager.getInstanceId(client) : null, + ); + } + + /// Helper method to convert instances ids to objects. + Future setBackgroundColorFromInstance(WebView instance, int color) { + return setBackgroundColor(instanceManager.getInstanceId(instance)!, color); + } +} + +/// Host api implementation for [WebSettings]. +class WebSettingsHostApiImpl extends WebSettingsHostApi { + /// Constructs a [WebSettingsHostApiImpl]. + WebSettingsHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late 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)!, + ); + } + } + + /// Helper method to convert instances ids to objects. + Future disposeFromInstance(WebSettings instance) async { + final int? instanceId = instanceManager.removeInstance(instance); + if (instanceId != null) { + return dispose(instanceId); + } + } + + /// Helper method to convert instances ids to objects. + Future setDomStorageEnabledFromInstance( + WebSettings instance, + bool flag, + ) { + return setDomStorageEnabled(instanceManager.getInstanceId(instance)!, flag); + } + + /// Helper method to convert instances ids to objects. + Future setJavaScriptCanOpenWindowsAutomaticallyFromInstance( + WebSettings instance, + bool flag, + ) { + return setJavaScriptCanOpenWindowsAutomatically( + instanceManager.getInstanceId(instance)!, + flag, + ); + } + + /// Helper method to convert instances ids to objects. + Future setSupportMultipleWindowsFromInstance( + WebSettings instance, + bool support, + ) { + return setSupportMultipleWindows( + instanceManager.getInstanceId(instance)!, support); + } + + /// Helper method to convert instances ids to objects. + Future setJavaScriptEnabledFromInstance( + WebSettings instance, + bool flag, + ) { + return setJavaScriptEnabled( + instanceManager.getInstanceId(instance)!, + flag, + ); + } + + /// Helper method to convert instances ids to objects. + Future setUserAgentStringFromInstance( + WebSettings instance, + String? userAgentString, + ) { + return setUserAgentString( + instanceManager.getInstanceId(instance)!, + userAgentString, + ); + } + + /// Helper method to convert instances ids to objects. + Future setMediaPlaybackRequiresUserGestureFromInstance( + WebSettings instance, + bool require, + ) { + return setMediaPlaybackRequiresUserGesture( + instanceManager.getInstanceId(instance)!, + require, + ); + } + + /// Helper method to convert instances ids to objects. + Future setSupportZoomFromInstance( + WebSettings instance, + bool support, + ) { + return setSupportZoom(instanceManager.getInstanceId(instance)!, support); + } + + /// Helper method to convert instances ids to objects. + Future setLoadWithOverviewModeFromInstance( + WebSettings instance, + bool overview, + ) { + return setLoadWithOverviewMode( + instanceManager.getInstanceId(instance)!, + overview, + ); + } + + /// Helper method to convert instances ids to objects. + Future setUseWideViewPortFromInstance( + WebSettings instance, + bool use, + ) { + return setUseWideViewPort(instanceManager.getInstanceId(instance)!, use); + } + + /// Helper method to convert instances ids to objects. + Future setDisplayZoomControlsFromInstance( + WebSettings instance, + bool enabled, + ) { + return setDisplayZoomControls( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } + + /// Helper method to convert instances ids to objects. + Future setBuiltInZoomControlsFromInstance( + WebSettings instance, + bool enabled, + ) { + return setBuiltInZoomControls( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } + + /// Helper method to convert instances ids to objects. + Future setAllowFileAccessFromInstance( + WebSettings instance, + bool enabled, + ) { + return setAllowFileAccess( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } +} + +/// Host api implementation for [JavaScriptChannel]. +class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { + /// Constructs a [JavaScriptChannelHostApiImpl]. + JavaScriptChannelHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late 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); + } + } +} + +/// Flutter api implementation for [JavaScriptChannel]. +class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { + /// Constructs a [JavaScriptChannelFlutterApiImpl]. + JavaScriptChannelFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + + @override + void postMessage(int instanceId, String message) { + final JavaScriptChannel? instance = + instanceManager.getInstance(instanceId) as JavaScriptChannel?; + assert( + instance != null, + 'InstanceManager does not contain an JavaScriptChannel with instanceId: $instanceId', + ); + instance!.postMessage(message); + } +} + +/// Host api implementation for [WebViewClient]. +class WebViewClientHostApiImpl extends WebViewClientHostApi { + /// Constructs a [WebViewClientHostApiImpl]. + WebViewClientHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late 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); + } + } +} + +/// Flutter api implementation for [WebViewClient]. +class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { + /// Constructs a [WebViewClientFlutterApiImpl]. + WebViewClientFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void dispose(int instanceId) { + instanceManager.removeInstance(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?; + assert( + instance != null, + 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + instance!.onPageFinished(webViewInstance!, url); + } + + @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?; + assert( + instance != null, + 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + instance!.onPageStarted(webViewInstance!, url); + } + + @override + void onReceivedError( + int instanceId, + int webViewInstanceId, + int errorCode, + String description, + String failingUrl, + ) { + final WebViewClient? instance = + instanceManager.getInstance(instanceId) as WebViewClient?; + final WebView? webViewInstance = + instanceManager.getInstance(webViewInstanceId) as WebView?; + assert( + instance != null, + 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + // ignore: deprecated_member_use_from_same_package + instance!.onReceivedError( + webViewInstance!, + errorCode, + description, + failingUrl, + ); + } + + @override + void onReceivedRequestError( + int instanceId, + int webViewInstanceId, + WebResourceRequestData request, + WebResourceErrorData error, + ) { + final WebViewClient? instance = + instanceManager.getInstance(instanceId) as WebViewClient?; + final WebView? webViewInstance = + instanceManager.getInstance(webViewInstanceId) as WebView?; + assert( + instance != null, + 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + instance!.onReceivedRequestError( + webViewInstance!, + _toWebResourceRequest(request), + _toWebResourceError(error), + ); + } + + @override + void requestLoading( + int instanceId, + int webViewInstanceId, + WebResourceRequestData request, + ) { + final WebViewClient? instance = + instanceManager.getInstance(instanceId) as WebViewClient?; + final WebView? webViewInstance = + instanceManager.getInstance(webViewInstanceId) as WebView?; + assert( + instance != null, + 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + instance!.requestLoading(webViewInstance!, _toWebResourceRequest(request)); + } + + @override + void urlLoading( + int instanceId, + int webViewInstanceId, + String url, + ) { + final WebViewClient? instance = + instanceManager.getInstance(instanceId) as WebViewClient?; + final WebView? webViewInstance = + instanceManager.getInstance(webViewInstanceId) as WebView?; + assert( + instance != null, + 'InstanceManager does not contain an WebViewClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + instance!.urlLoading(webViewInstance!, url); + } +} + +/// Host api implementation for [DownloadListener]. +class DownloadListenerHostApiImpl extends DownloadListenerHostApi { + /// Constructs a [DownloadListenerHostApiImpl]. + DownloadListenerHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late 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); + } + } +} + +/// Flutter api implementation for [DownloadListener]. +class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { + /// Constructs a [DownloadListenerFlutterApiImpl]. + DownloadListenerFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + + @override + void onDownloadStart( + int instanceId, + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + final DownloadListener? instance = + instanceManager.getInstance(instanceId) as DownloadListener?; + assert( + instance != null, + 'InstanceManager does not contain an DownloadListener with instanceId: $instanceId', + ); + instance!.onDownloadStart( + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + ); + } +} + +/// Host api implementation for [DownloadListener]. +class WebChromeClientHostApiImpl extends WebChromeClientHostApi { + /// Constructs a [WebChromeClientHostApiImpl]. + WebChromeClientHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late 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)!); + } + } +} + +/// Flutter api implementation for [DownloadListener]. +class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { + /// Constructs a [DownloadListenerFlutterApiImpl]. + WebChromeClientFlutterApiImpl({InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + @override + void dispose(int instanceId) { + instanceManager.removeInstance(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?; + assert( + instance != null, + 'InstanceManager does not contain an WebChromeClient with instanceId: $instanceId', + ); + assert( + webViewInstance != null, + 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', + ); + instance!.onProgressChanged(webViewInstance!, progress); + } +} + +/// Host api implementation for [WebStorage]. +class WebStorageHostApiImpl extends WebStorageHostApi { + /// Constructs a [WebStorageHostApiImpl]. + WebStorageHostApiImpl({ + BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + }) : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? InstanceManager.instance; + } + + /// Maintains instances stored to communicate with java objects. + late final InstanceManager instanceManager; + + /// Helper method to convert instances ids to objects. + Future createFromInstance(WebStorage instance) async { + final int? instanceId = instanceManager.tryAddInstance(instance); + if (instanceId != null) { + return create(instanceId); + } + } + + /// Helper method to convert instances ids to objects. + Future deleteAllDataFromInstance(WebStorage instance) { + return deleteAllData(instanceManager.getInstanceId(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 new file mode 100644 index 000000000000..e6dd2b2c2c1d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/instance_manager.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. + +/// Maintains instances stored to communicate with java objects. +class InstanceManager { + final Map _instanceIdsToInstances = {}; + final Map _instancesToInstanceIds = {}; + + int _nextInstanceId = 0; + + /// Global instance of [InstanceManager]. + static final InstanceManager instance = InstanceManager(); + + /// Attempt to add a new instance. + /// + /// 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)) { + return null; + } + + final int instanceId = _nextInstanceId++; + _instancesToInstanceIds[instance] = instanceId; + _instanceIdsToInstances[instanceId] = instance; + return instanceId; + } + + /// Remove the instance 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); + } + return instanceId; + } + + /// Retrieve the Object paired with instanceId. + Object? getInstance(int instanceId) { + return _instanceIdsToInstances[instanceId]; + } + + /// Retrieve the instanceId paired with instance. + int? getInstanceId(Object instance) { + return _instancesToInstanceIds[instance]; + } +} 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 a48e457d55ad..1f0eb7bd7ded 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -10,6 +10,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_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. /// /// This is used as the default implementation for [WebView.platform] on Android. It uses @@ -25,38 +29,47 @@ class AndroidWebView implements WebViewPlatform { WebViewPlatformCreatedCallback? onWebViewPlatformCreated, Set>? gestureRecognizers, }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, - webViewPlatformCallbacksHandler, - javascriptChannelRegistry, - )); - }, - gestureRecognizers: gestureRecognizers, - layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ), + return WebViewAndroidWidget( + useHybridComposition: false, + creationParams: creationParams, + callbacksHandler: webViewPlatformCallbacksHandler, + javascriptChannelRegistry: javascriptChannelRegistry, + onBuildWidget: (WebViewAndroidPlatformController controller) { + return GestureDetector( + // We prevent text selection by intercepting the long press event. + // This is a temporary stop gap due to issues with text selection on Android: + // https://github.com/flutter/flutter/issues/24585 - the text selection + // dialog is not responding to touch events. + // https://github.com/flutter/flutter/issues/24584 - the text selection + // handles are not showing. + // TODO(amirh): remove this when the issues above are fixed. + onLongPress: () {}, + excludeFromSemantics: true, + child: AndroidView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(controller); + } + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: + Directionality.maybeOf(context) ?? TextDirection.rtl, + creationParams: + InstanceManager.instance.getInstanceId(controller.webView), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + }, ); } @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } } 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 new file mode 100644 index 000000000000..bba75ff4f613 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart @@ -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 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() => + android_webview.CookieManager.instance.clearCookies(); + + @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 android_webview.CookieManager.instance.setCookie( + cookie.domain, + '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; 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; + } +} 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 new file mode 100644 index 000000000000..ff6265dbad00 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart @@ -0,0 +1,727 @@ +// Copyright 2013 The Flutter Authors. 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 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'src/android_webview.dart' as android_webview; + +/// Creates a [Widget] with a [android_webview.WebView]. +class WebViewAndroidWidget extends StatefulWidget { + /// Constructs a [WebViewAndroidWidget]. + const WebViewAndroidWidget({ + Key? key, + required this.creationParams, + required this.useHybridComposition, + required this.callbacksHandler, + required this.javascriptChannelRegistry, + required this.onBuildWidget, + @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; + + /// Whether the [android_webview.WebView] will be rendered with an [AndroidViewSurface]. + /// + /// This implementation uses hybrid composition to render the + /// [WebViewAndroidWidget]. This comes at the cost of some performance on + /// Android versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance + /// for more information. + /// + /// Defaults to false. + final bool useHybridComposition; + + /// Handles callbacks that are made by [android_webview.WebViewClient], [android_webview.DownloadListener], and [android_webview.WebChromeClient]. + final WebViewPlatformCallbacksHandler callbacksHandler; + + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + /// Handles constructing [android_webview.WebView]s and calling static methods. + /// + /// This should only be changed for testing purposes. + final WebViewProxy webViewProxy; + + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + + /// Callback to build a widget once [android_webview.WebView] has been initialized. + final Widget Function(WebViewAndroidPlatformController controller) + onBuildWidget; + + /// Manages the JavaScript storage APIs. + final android_webview.WebStorage? webStorage; + + @override + State createState() => _WebViewAndroidWidgetState(); +} + +class _WebViewAndroidWidgetState extends State { + late final WebViewAndroidPlatformController controller; + + @override + void initState() { + super.initState(); + controller = WebViewAndroidPlatformController( + useHybridComposition: widget.useHybridComposition, + creationParams: widget.creationParams, + callbacksHandler: widget.callbacksHandler, + javascriptChannelRegistry: widget.javascriptChannelRegistry, + webViewProxy: widget.webViewProxy, + flutterAssetManager: widget.flutterAssetManager, + webStorage: widget.webStorage, + ); + } + + @override + void dispose() { + super.dispose(); + controller._dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.onBuildWidget(controller); + } +} + +/// Implementation of [WebViewPlatformController] with the Android WebView api. +class WebViewAndroidPlatformController extends WebViewPlatformController { + /// Construct a [WebViewAndroidPlatformController]. + WebViewAndroidPlatformController({ + required bool useHybridComposition, + required CreationParams creationParams, + required this.callbacksHandler, + required this.javascriptChannelRegistry, + @visibleForTesting this.webViewProxy = const WebViewProxy(), + @visibleForTesting + this.flutterAssetManager = const android_webview.FlutterAssetManager(), + @visibleForTesting android_webview.WebStorage? webStorage, + }) : webStorage = webStorage ?? android_webview.WebStorage.instance, + assert(creationParams.webSettings?.hasNavigationDelegate != null), + super(callbacksHandler) { + webView = webViewProxy.createWebView( + useHybridComposition: useHybridComposition, + ); + + webView.settings.setDomStorageEnabled(true); + webView.settings.setJavaScriptCanOpenWindowsAutomatically(true); + webView.settings.setSupportMultipleWindows(true); + webView.settings.setLoadWithOverviewMode(true); + webView.settings.setUseWideViewPort(true); + webView.settings.setDisplayZoomControls(false); + webView.settings.setBuiltInZoomControls(true); + + _setCreationParams(creationParams); + webView.setDownloadListener(downloadListener); + webView.setWebChromeClient(webChromeClient); + + final String? initialUrl = creationParams.initialUrl; + if (initialUrl != null) { + loadUrl(initialUrl, {}); + } + } + + final Map _javaScriptChannels = + {}; + + late WebViewAndroidWebViewClient _webViewClient; + + /// Represents the WebView maintained by platform code. + late final android_webview.WebView webView; + + /// Handles callbacks that are made by [android_webview.WebViewClient], [android_webview.DownloadListener], and [android_webview.WebChromeClient]. + final WebViewPlatformCallbacksHandler callbacksHandler; + + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + /// Handles constructing [android_webview.WebView]s and calling static methods. + /// + /// This should only be changed for testing purposes. + final WebViewProxy webViewProxy; + + /// Manages access to Flutter assets that are part of the Android App bundle. + /// + /// This should only be changed for testing purposes. + final android_webview.FlutterAssetManager flutterAssetManager; + + /// Receives callbacks when content should be downloaded instead. + @visibleForTesting + late final WebViewAndroidDownloadListener downloadListener = + WebViewAndroidDownloadListener(loadUrl: loadUrl); + + /// Handles JavaScript dialogs, favicons, titles, new windows, and the progress for [android_webview.WebView]. + @visibleForTesting + 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; + + @override + Future loadHtmlString(String html, {String? baseUrl}) { + return webView.loadDataWithBaseUrl( + baseUrl: baseUrl, + data: html, + mimeType: 'text/html', + ); + } + + @override + Future loadFile(String absoluteFilePath) { + final String url = absoluteFilePath.startsWith('file://') + ? absoluteFilePath + : 'file://$absoluteFilePath'; + + webView.settings.setAllowFileAccess(true); + return webView.loadUrl(url, {}); + } + + @override + Future loadFlutterAsset(String key) async { + final String assetFilePath = + await flutterAssetManager.getAssetFilePathByName(key); + final List pathElements = assetFilePath.split('/'); + final String fileName = pathElements.removeLast(); + final List paths = + await flutterAssetManager.list(pathElements.join('/')); + + if (!paths.contains(fileName)) { + throw ArgumentError( + 'Asset for key "$key" not found.', + 'key', + ); + } + + return webView.loadUrl( + 'file:///android_asset/$assetFilePath', + {}, + ); + } + + @override + Future loadUrl( + String url, + Map? headers, + ) { + return webView.loadUrl(url, headers ?? {}); + } + + /// When making a POST request, headers are ignored. As a workaround, make + /// the request manually and load the response data using [loadHTMLString]. + @override + Future loadRequest( + WebViewRequest request, + ) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + switch (request.method) { + case WebViewRequestMethod.get: + return webView.loadUrl(request.uri.toString(), request.headers); + case WebViewRequestMethod.post: + return webView.postUrl( + request.uri.toString(), request.body ?? Uint8List(0)); + default: + throw UnimplementedError( + 'This version of webview_android_widget currently has no implementation for HTTP method ${request.method.serialize()} in loadRequest.', + ); + } + } + + @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() { + webView.clearCache(true); + return webStorage.deleteAllData(); + } + + @override + Future updateSettings(WebSettings setting) async { + await Future.wait(>[ + _setUserAgent(setting.userAgent), + if (setting.hasProgressTracking != null) + _setHasProgressTracking(setting.hasProgressTracking!), + if (setting.hasNavigationDelegate != null) + _setHasNavigationDelegate(setting.hasNavigationDelegate!), + if (setting.javascriptMode != null) + _setJavaScriptMode(setting.javascriptMode!), + if (setting.debuggingEnabled != null) + _setDebuggingEnabled(setting.debuggingEnabled!), + if (setting.zoomEnabled != null) _setZoomEnabled(setting.zoomEnabled!), + ]); + } + + @override + Future evaluateJavascript(String javascript) async { + return runJavascriptReturningResult(javascript); + } + + @override + Future runJavascript(String javascript) async { + await webView.evaluateJavascript(javascript); + } + + @override + Future runJavascriptReturningResult(String javascript) async { + return await webView.evaluateJavascript(javascript) ?? ''; + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + return Future.wait( + javascriptChannelNames.where( + (String channelName) { + return !_javaScriptChannels.containsKey(channelName); + }, + ).map>( + (String channelName) { + final WebViewAndroidJavaScriptChannel javaScriptChannel = + WebViewAndroidJavaScriptChannel( + channelName, javascriptChannelRegistry); + _javaScriptChannels[channelName] = javaScriptChannel; + return webView.addJavaScriptChannel(javaScriptChannel); + }, + ), + ); + } + + @override + Future removeJavascriptChannels( + Set javascriptChannelNames, + ) { + return Future.wait( + javascriptChannelNames.where( + (String channelName) { + return _javaScriptChannels.containsKey(channelName); + }, + ).map>( + (String channelName) { + final WebViewAndroidJavaScriptChannel javaScriptChannel = + _javaScriptChannels[channelName]!; + _javaScriptChannels.remove(channelName); + return webView.removeJavaScriptChannel(javaScriptChannel); + }, + ), + ); + } + + @override + Future getTitle() => webView.getTitle(); + + @override + Future scrollTo(int x, int y) => webView.scrollTo(x, y); + + @override + Future scrollBy(int x, int y) => webView.scrollBy(x, y); + + @override + Future getScrollX() => webView.getScrollX(); + + @override + Future getScrollY() => webView.getScrollY(); + + Future _dispose() => webView.release(); + + void _setCreationParams(CreationParams creationParams) { + final WebSettings? webSettings = creationParams.webSettings; + if (webSettings != null) { + updateSettings(webSettings); + } + + final String? userAgent = creationParams.userAgent; + if (userAgent != null) { + webView.settings.setUserAgentString(userAgent); + } + + webView.settings.setMediaPlaybackRequiresUserGesture( + creationParams.autoMediaPlaybackPolicy != + AutoMediaPlaybackPolicy.always_allow, + ); + + final Color? backgroundColor = creationParams.backgroundColor; + if (backgroundColor != null) { + webView.setBackgroundColor(backgroundColor); + } + + addJavascriptChannels(creationParams.javascriptChannelNames); + + // TODO(BeMacized): Remove once platform implementations + // are able to register themselves (Flutter >=2.8), + // https://github.com/flutter/flutter/issues/94224 + WebViewCookieManagerPlatform.instance ??= WebViewAndroidCookieManager(); + + creationParams.cookies + .forEach(WebViewCookieManagerPlatform.instance!.setCookie); + } + + Future _setHasProgressTracking(bool hasProgressTracking) async { + if (hasProgressTracking) { + webChromeClient._onProgress = callbacksHandler.onProgress; + } else { + webChromeClient._onProgress = null; + } + } + + Future _setHasNavigationDelegate(bool hasNavigationDelegate) { + if (hasNavigationDelegate) { + downloadListener._onNavigationRequest = + callbacksHandler.onNavigationRequest; + _webViewClient = WebViewAndroidWebViewClient.handlesNavigation( + onPageStartedCallback: callbacksHandler.onPageStarted, + onPageFinishedCallback: callbacksHandler.onPageFinished, + onWebResourceErrorCallback: callbacksHandler.onWebResourceError, + loadUrl: loadUrl, + onNavigationRequestCallback: callbacksHandler.onNavigationRequest, + ); + } else { + downloadListener._onNavigationRequest = null; + _webViewClient = WebViewAndroidWebViewClient( + onPageStartedCallback: callbacksHandler.onPageStarted, + onPageFinishedCallback: callbacksHandler.onPageFinished, + onWebResourceErrorCallback: callbacksHandler.onWebResourceError, + ); + } + return webView.setWebViewClient(_webViewClient); + } + + Future _setJavaScriptMode(JavascriptMode mode) { + switch (mode) { + case JavascriptMode.disabled: + return webView.settings.setJavaScriptEnabled(false); + case JavascriptMode.unrestricted: + return webView.settings.setJavaScriptEnabled(true); + } + } + + Future _setDebuggingEnabled(bool debuggingEnabled) { + return webViewProxy.setWebContentsDebuggingEnabled(debuggingEnabled); + } + + Future _setUserAgent(WebSetting userAgent) { + if (userAgent.isPresent) { + // If the string is empty, the system default value will be used. + return webView.settings.setUserAgentString(userAgent.value ?? ''); + } + + return Future.value(); + } + + Future _setZoomEnabled(bool zoomEnabled) { + return webView.settings.setSupportZoom(zoomEnabled); + } +} + +/// Exposes a channel to receive calls from javaScript. +class WebViewAndroidJavaScriptChannel + extends android_webview.JavaScriptChannel { + /// Creates a [WebViewAndroidJavaScriptChannel]. + WebViewAndroidJavaScriptChannel( + String channelName, this.javascriptChannelRegistry) + : super(channelName); + + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + @override + void postMessage(String message) { + javascriptChannelRegistry.onJavascriptChannelMessage(channelName, message); + } +} + +/// Receives callbacks when content can not be handled by the rendering engine for [WebViewAndroidPlatformController], and should be downloaded instead. +/// +/// When handling navigation requests, this calls [onNavigationRequestCallback] +/// when a [android_webview.WebView] attempts to navigate to a new page. If +/// this callback return true, this calls [loadUrl]. +class WebViewAndroidDownloadListener extends android_webview.DownloadListener { + /// Creates a [WebViewAndroidDownloadListener]. + WebViewAndroidDownloadListener({required this.loadUrl}); + + // Changed by WebViewAndroidPlatformController. + FutureOr Function({ + required String url, + required bool isForMainFrame, + })? _onNavigationRequest; + + /// Callback to load a URL when a navigation request is approved. + final Future Function(String url, Map? headers) loadUrl; + + @override + void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + if (_onNavigationRequest == null) { + return; + } + + final FutureOr returnValue = _onNavigationRequest!( + url: url, + isForMainFrame: true, + ); + + if (returnValue is bool && returnValue) { + loadUrl(url, {}); + } else { + (returnValue as Future).then((bool shouldLoadUrl) { + if (shouldLoadUrl) { + loadUrl(url, {}); + } + }); + } + } +} + +/// Receives various navigation requests and errors for [WebViewAndroidPlatformController]. +/// +/// When handling navigation requests, this calls [onNavigationRequestCallback] +/// when a [android_webview.WebView] attempts to navigate to a new page. If +/// this callback return true, this calls [loadUrl]. +class WebViewAndroidWebViewClient extends android_webview.WebViewClient { + /// Creates a [WebViewAndroidWebViewClient] that doesn't handle navigation requests. + WebViewAndroidWebViewClient({ + required this.onPageStartedCallback, + required this.onPageFinishedCallback, + required this.onWebResourceErrorCallback, + }) : loadUrl = null, + onNavigationRequestCallback = null, + super(shouldOverrideUrlLoading: false); + + /// Creates a [WebViewAndroidWebViewClient] that handles navigation requests. + WebViewAndroidWebViewClient.handlesNavigation({ + required this.onPageStartedCallback, + required this.onPageFinishedCallback, + required this.onWebResourceErrorCallback, + required this.onNavigationRequestCallback, + required this.loadUrl, + }) : super(shouldOverrideUrlLoading: true); + + /// Callback when [android_webview.WebViewClient] receives a callback from [android_webview.WebViewClient].onPageStarted. + final void Function(String url) onPageStartedCallback; + + /// Callback when [android_webview.WebViewClient] receives a callback from [android_webview.WebViewClient].onPageFinished. + final void Function(String url) onPageFinishedCallback; + + /// Callback when [android_webview.WebViewClient] receives an error callback. + void Function(WebResourceError error) onWebResourceErrorCallback; + + /// Checks whether a navigation request should be approved or disaproved. + final FutureOr Function({ + required String url, + required bool isForMainFrame, + })? onNavigationRequestCallback; + + /// Callback when a navigation request is approved. + final Future Function(String url, Map? headers)? + loadUrl; + + static WebResourceErrorType _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case android_webview.WebViewClient.errorAuthentication: + return WebResourceErrorType.authentication; + case android_webview.WebViewClient.errorBadUrl: + return WebResourceErrorType.badUrl; + case android_webview.WebViewClient.errorConnect: + return WebResourceErrorType.connect; + case android_webview.WebViewClient.errorFailedSslHandshake: + return WebResourceErrorType.failedSslHandshake; + case android_webview.WebViewClient.errorFile: + return WebResourceErrorType.file; + case android_webview.WebViewClient.errorFileNotFound: + return WebResourceErrorType.fileNotFound; + case android_webview.WebViewClient.errorHostLookup: + return WebResourceErrorType.hostLookup; + case android_webview.WebViewClient.errorIO: + return WebResourceErrorType.io; + case android_webview.WebViewClient.errorProxyAuthentication: + return WebResourceErrorType.proxyAuthentication; + case android_webview.WebViewClient.errorRedirectLoop: + return WebResourceErrorType.redirectLoop; + case android_webview.WebViewClient.errorTimeout: + return WebResourceErrorType.timeout; + case android_webview.WebViewClient.errorTooManyRequests: + return WebResourceErrorType.tooManyRequests; + case android_webview.WebViewClient.errorUnknown: + return WebResourceErrorType.unknown; + case android_webview.WebViewClient.errorUnsafeResource: + return WebResourceErrorType.unsafeResource; + case android_webview.WebViewClient.errorUnsupportedAuthScheme: + return WebResourceErrorType.unsupportedAuthScheme; + case android_webview.WebViewClient.errorUnsupportedScheme: + return WebResourceErrorType.unsupportedScheme; + } + + throw ArgumentError( + 'Could not find a WebResourceErrorType for errorCode: $errorCode', + ); + } + + /// Whether this [android_webview.WebViewClient] handles navigation requests. + bool get handlesNavigation => + loadUrl != null && onNavigationRequestCallback != null; + + @override + void onPageStarted(android_webview.WebView webView, String url) { + onPageStartedCallback(url); + } + + @override + void onPageFinished(android_webview.WebView webView, String url) { + onPageFinishedCallback(url); + } + + @override + void onReceivedError( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + ) { + onWebResourceErrorCallback(WebResourceError( + errorCode: errorCode, + description: description, + failingUrl: failingUrl, + errorType: _errorCodeToErrorType(errorCode), + )); + } + + @override + void onReceivedRequestError( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + ) { + if (request.isForMainFrame) { + onWebResourceErrorCallback(WebResourceError( + errorCode: error.errorCode, + description: error.description, + failingUrl: request.url, + errorType: _errorCodeToErrorType(error.errorCode), + )); + } + } + + @override + void urlLoading(android_webview.WebView webView, String url) { + if (!handlesNavigation) { + return; + } + + final FutureOr returnValue = onNavigationRequestCallback!( + url: url, + isForMainFrame: true, + ); + + if (returnValue is bool && returnValue) { + loadUrl!(url, {}); + } else if (returnValue is Future) { + returnValue.then((bool shouldLoadUrl) { + if (shouldLoadUrl) { + loadUrl!(url, {}); + } + }); + } + } + + @override + void requestLoading( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + ) { + if (!handlesNavigation) { + return; + } + + final FutureOr returnValue = onNavigationRequestCallback!( + url: request.url, + isForMainFrame: request.isForMainFrame, + ); + + if (returnValue is bool && returnValue) { + loadUrl!(request.url, {}); + } else if (returnValue is Future) { + returnValue.then((bool shouldLoadUrl) { + if (shouldLoadUrl) { + loadUrl!(request.url, {}); + } + }); + } + } +} + +/// Handles JavaScript dialogs, favicons, titles, and the progress for [WebViewAndroidPlatformController]. +class WebViewAndroidWebChromeClient extends android_webview.WebChromeClient { + // Changed by WebViewAndroidPlatformController. + void Function(int progress)? _onProgress; + + @override + void onProgressChanged(android_webview.WebView webView, int progress) { + if (_onProgress != null) { + _onProgress!(progress); + } + } +} + +/// Handles constructing [android_webview.WebView]s and calling static methods. +/// +/// This should only be used for testing purposes. +@visibleForTesting +class WebViewProxy { + /// Creates a [WebViewProxy]. + const WebViewProxy(); + + /// Constructs a [android_webview.WebView]. + android_webview.WebView createWebView({required bool useHybridComposition}) { + return android_webview.WebView(useHybridComposition: useHybridComposition); + } + + /// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. + /// + /// This flag can be enabled in order to facilitate debugging of web layouts + /// and JavaScript code running inside WebViews. Please refer to + /// [android_webview.WebView] documentation for the debugging guide. The + /// default is false. + /// + /// See [android_webview.WebView].setWebContentsDebuggingEnabled. + Future setWebContentsDebuggingEnabled(bool enabled) { + return android_webview.WebView.setWebContentsDebuggingEnabled(enabled); + } +} 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 6beae105e2e5..00d7c8c53b7f 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 @@ -9,7 +9,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_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. /// @@ -30,48 +33,46 @@ class SurfaceAndroidWebView extends AndroidWebView { Set>? gestureRecognizers, required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, }) { - assert(webViewPlatformCallbacksHandler != null); - return PlatformViewLink( - viewType: 'plugins.flutter.io/webview', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, + return WebViewAndroidWidget( + useHybridComposition: true, + creationParams: creationParams, + callbacksHandler: webViewPlatformCallbacksHandler, + javascriptChannelRegistry: javascriptChannelRegistry, + onBuildWidget: (WebViewAndroidPlatformController controller) { + return PlatformViewLink( 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: MethodChannelWebViewPlatform.creationParamsToMap( - creationParams, - usesHybridComposition: true, - ), - creationParamsCodec: const StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform( - id, - webViewPlatformCallbacksHandler, - javascriptChannelRegistry, - ), + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, ); - }) - ..create(); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + 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(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((int id) { + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(controller); + } + }) + ..create(); + }, + ); }, ); } diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart new file mode 100644 index 000000000000..70ecd99d3638 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -0,0 +1,264 @@ +// Copyright 2013 The Flutter Authors. 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/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 { + 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; +} + +class WebResourceErrorData { + WebResourceErrorData(this.errorCode, this.description); + + int errorCode; + String description; +} + +@HostApi() +abstract class CookieManagerHostApi { + @async + bool clearCookies(); + + void setCookie(String url, String value); +} + +@HostApi(dartHostTestHandler: 'TestWebViewHostApi') +abstract class WebViewHostApi { + 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 loadUrl( + int instanceId, + String url, + Map headers, + ); + + void postUrl( + int instanceId, + String url, + Uint8List data, + ); + + String? getUrl(int instanceId); + + bool canGoBack(int instanceId); + + bool canGoForward(int instanceId); + + void goBack(int instanceId); + + void goForward(int instanceId); + + void reload(int instanceId); + + void clearCache(int instanceId, bool includeDiskFiles); + + @async + String? 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); + + void setWebContentsDebuggingEnabled(bool enabled); + + void setWebViewClient(int instanceId, int webViewClientInstanceId); + + void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + + void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + + void setDownloadListener(int instanceId, int? listenerInstanceId); + + void setWebChromeClient(int instanceId, int? clientInstanceId); + + void setBackgroundColor(int instanceId, int color); +} + +@HostApi(dartHostTestHandler: 'TestWebSettingsHostApi') +abstract class WebSettingsHostApi { + void create(int instanceId, int webViewInstanceId); + + void dispose(int instanceId); + + void setDomStorageEnabled(int instanceId, bool flag); + + void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag); + + void setSupportMultipleWindows(int instanceId, bool support); + + void setJavaScriptEnabled(int instanceId, bool flag); + + void setUserAgentString(int instanceId, String? userAgentString); + + void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); + + void setSupportZoom(int instanceId, bool support); + + void setLoadWithOverviewMode(int instanceId, bool overview); + + void setUseWideViewPort(int instanceId, bool use); + + void setDisplayZoomControls(int instanceId, bool enabled); + + void setBuiltInZoomControls(int instanceId, bool enabled); + + void setAllowFileAccess(int instanceId, bool enabled); +} + +@HostApi(dartHostTestHandler: 'TestJavaScriptChannelHostApi') +abstract class JavaScriptChannelHostApi { + void create(int instanceId, String channelName); +} + +@FlutterApi() +abstract class JavaScriptChannelFlutterApi { + void dispose(int instanceId); + + void postMessage(int instanceId, String message); +} + +@HostApi(dartHostTestHandler: 'TestWebViewClientHostApi') +abstract class WebViewClientHostApi { + void create(int instanceId, bool shouldOverrideUrlLoading); +} + +@FlutterApi() +abstract class WebViewClientFlutterApi { + 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 urlLoading(int instanceId, int webViewInstanceId, String url); +} + +@HostApi(dartHostTestHandler: 'TestDownloadListenerHostApi') +abstract class DownloadListenerHostApi { + void create(int instanceId); +} + +@FlutterApi() +abstract class DownloadListenerFlutterApi { + void dispose(int instanceId); + + void onDownloadStart( + int instanceId, + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ); +} + +@HostApi(dartHostTestHandler: 'TestWebChromeClientHostApi') +abstract class WebChromeClientHostApi { + void create(int instanceId, int webViewClientInstanceId); +} + +@HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') +abstract class FlutterAssetManagerHostApi { + List list(String path); + + String getAssetFilePathByName(String name); +} + +@FlutterApi() +abstract class WebChromeClientFlutterApi { + void dispose(int instanceId); + + 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 f7db4c6fb63a..a386cad9028c 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -1,12 +1,12 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. -repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android +repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.13 +version: 2.8.14 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: @@ -19,13 +19,14 @@ flutter: dependencies: flutter: sdk: flutter - - webview_flutter_platform_interface: ^1.0.0 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: + build_runner: ^2.1.4 flutter_driver: sdk: flutter flutter_test: sdk: flutter + mockito: ^5.1.0 pedantic: ^1.10.0 - + pigeon: ^3.0.3 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 new file mode 100644 index 000000000000..4c63ab025702 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -0,0 +1,710 @@ +// Copyright 2013 The Flutter Authors. 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_android/src/android_webview.dart'; +import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; + +import 'android_webview_test.mocks.dart'; +import 'test_android_webview.pigeon.dart'; + +@GenerateMocks([ + CookieManagerHostApi, + DownloadListener, + JavaScriptChannel, + TestDownloadListenerHostApi, + TestJavaScriptChannelHostApi, + TestWebChromeClientHostApi, + TestWebSettingsHostApi, + TestWebStorageHostApi, + TestWebViewClientHostApi, + TestWebViewHostApi, + TestAssetManagerHostApi, + WebChromeClient, + WebView, + WebViewClient, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Android WebView', () { + group('WebView', () { + late MockTestWebViewHostApi mockPlatformHostApi; + + late InstanceManager instanceManager; + + late WebView webView; + late int webViewInstanceId; + + setUp(() { + mockPlatformHostApi = MockTestWebViewHostApi(); + TestWebViewHostApi.setup(mockPlatformHostApi); + + instanceManager = InstanceManager(); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + + webView = WebView(); + webViewInstanceId = instanceManager.getInstanceId(webView)!; + }); + + test('create', () { + verify(mockPlatformHostApi.create(webViewInstanceId, false)); + }); + + test('setWebContentsDebuggingEnabled true', () { + WebView.setWebContentsDebuggingEnabled(true); + verify(mockPlatformHostApi.setWebContentsDebuggingEnabled(true)); + }); + + test('setWebContentsDebuggingEnabled false', () { + WebView.setWebContentsDebuggingEnabled(false); + verify(mockPlatformHostApi.setWebContentsDebuggingEnabled(false)); + }); + + test('loadData', () { + webView.loadData( + data: 'hello', + mimeType: 'text/plain', + encoding: 'base64', + ); + verify(mockPlatformHostApi.loadData( + webViewInstanceId, + 'hello', + 'text/plain', + 'base64', + )); + }); + + test('loadData with null values', () { + webView.loadData(data: 'hello', mimeType: null, encoding: null); + verify(mockPlatformHostApi.loadData( + webViewInstanceId, + 'hello', + null, + null, + )); + }); + + test('loadDataWithBaseUrl', () { + webView.loadDataWithBaseUrl( + baseUrl: 'https://base.url', + data: 'hello', + mimeType: 'text/plain', + encoding: 'base64', + historyUrl: 'https://history.url', + ); + + verify(mockPlatformHostApi.loadDataWithBaseUrl( + webViewInstanceId, + 'https://base.url', + 'hello', + 'text/plain', + 'base64', + 'https://history.url', + )); + }); + + test('loadDataWithBaseUrl with null values', () { + webView.loadDataWithBaseUrl(data: 'hello'); + verify(mockPlatformHostApi.loadDataWithBaseUrl( + webViewInstanceId, + null, + 'hello', + null, + null, + null, + )); + }); + + test('loadUrl', () { + webView.loadUrl('hello', {'a': 'header'}); + verify(mockPlatformHostApi.loadUrl( + webViewInstanceId, + 'hello', + {'a': 'header'}, + )); + }); + + test('canGoBack', () { + when(mockPlatformHostApi.canGoBack(webViewInstanceId)) + .thenReturn(false); + expect(webView.canGoBack(), completion(false)); + }); + + test('canGoForward', () { + when(mockPlatformHostApi.canGoForward(webViewInstanceId)) + .thenReturn(true); + expect(webView.canGoForward(), completion(true)); + }); + + 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('clearCache', () { + webView.clearCache(false); + verify(mockPlatformHostApi.clearCache(webViewInstanceId, false)); + }); + + test('evaluateJavascript', () { + when( + mockPlatformHostApi.evaluateJavascript( + webViewInstanceId, 'runJavaScript'), + ).thenAnswer((_) => Future.value('returnValue')); + expect( + webView.evaluateJavascript('runJavaScript'), + completion('returnValue'), + ); + }); + + test('getTitle', () { + when(mockPlatformHostApi.getTitle(webViewInstanceId)) + .thenReturn('aTitle'); + expect(webView.getTitle(), completion('aTitle')); + }); + + test('scrollTo', () { + webView.scrollTo(12, 13); + verify(mockPlatformHostApi.scrollTo(webViewInstanceId, 12, 13)); + }); + + test('scrollBy', () { + webView.scrollBy(12, 14); + verify(mockPlatformHostApi.scrollBy(webViewInstanceId, 12, 14)); + }); + + test('getScrollX', () { + when(mockPlatformHostApi.getScrollX(webViewInstanceId)).thenReturn(67); + expect(webView.getScrollX(), completion(67)); + }); + + test('getScrollY', () { + when(mockPlatformHostApi.getScrollY(webViewInstanceId)).thenReturn(56); + expect(webView.getScrollY(), completion(56)); + }); + + test('setWebViewClient', () { + TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); + WebViewClient.api = WebViewClientHostApiImpl( + instanceManager: instanceManager, + ); + + final WebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); + webView.setWebViewClient(mockWebViewClient); + + final int webViewClientInstanceId = + instanceManager.getInstanceId(mockWebViewClient)!; + verify(mockPlatformHostApi.setWebViewClient( + webViewInstanceId, + webViewClientInstanceId, + )); + }); + + test('addJavaScriptChannel', () { + TestJavaScriptChannelHostApi.setup(MockTestJavaScriptChannelHostApi()); + JavaScriptChannel.api = JavaScriptChannelHostApiImpl( + instanceManager: instanceManager, + ); + + final JavaScriptChannel mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.channelName).thenReturn('aChannel'); + + webView.addJavaScriptChannel(mockJavaScriptChannel); + + final int javaScriptChannelInstanceId = + instanceManager.getInstanceId(mockJavaScriptChannel)!; + verify(mockPlatformHostApi.addJavaScriptChannel( + webViewInstanceId, + javaScriptChannelInstanceId, + )); + }); + + test('removeJavaScriptChannel', () { + TestJavaScriptChannelHostApi.setup(MockTestJavaScriptChannelHostApi()); + JavaScriptChannel.api = JavaScriptChannelHostApiImpl( + instanceManager: instanceManager, + ); + + final JavaScriptChannel mockJavaScriptChannel = MockJavaScriptChannel(); + when(mockJavaScriptChannel.channelName).thenReturn('aChannel'); + + expect( + webView.removeJavaScriptChannel(mockJavaScriptChannel), + completes, + ); + + webView.addJavaScriptChannel(mockJavaScriptChannel); + webView.removeJavaScriptChannel(mockJavaScriptChannel); + + final int javaScriptChannelInstanceId = + instanceManager.getInstanceId(mockJavaScriptChannel)!; + verify(mockPlatformHostApi.removeJavaScriptChannel( + webViewInstanceId, + javaScriptChannelInstanceId, + )); + }); + + test('setDownloadListener', () { + TestDownloadListenerHostApi.setup(MockTestDownloadListenerHostApi()); + DownloadListener.api = DownloadListenerHostApiImpl( + instanceManager: instanceManager, + ); + + final DownloadListener mockDownloadListener = MockDownloadListener(); + webView.setDownloadListener(mockDownloadListener); + + final int downloadListenerInstanceId = + instanceManager.getInstanceId(mockDownloadListener)!; + verify(mockPlatformHostApi.setDownloadListener( + webViewInstanceId, + downloadListenerInstanceId, + )); + }); + + test('setWebChromeClient', () { + // Setting a WebChromeClient requires setting a WebViewClient first. + TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); + WebViewClient.api = WebViewClientHostApiImpl( + instanceManager: instanceManager, + ); + final WebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); + webView.setWebViewClient(mockWebViewClient); + + TestWebChromeClientHostApi.setup(MockTestWebChromeClientHostApi()); + WebChromeClient.api = WebChromeClientHostApiImpl( + instanceManager: instanceManager, + ); + + final WebChromeClient mockWebChromeClient = MockWebChromeClient(); + webView.setWebChromeClient(mockWebChromeClient); + + final int webChromeClientInstanceId = + instanceManager.getInstanceId(mockWebChromeClient)!; + verify(mockPlatformHostApi.setWebChromeClient( + webViewInstanceId, + webChromeClientInstanceId, + )); + }); + + test('release', () { + final MockTestWebSettingsHostApi mockWebSettingsPlatformHostApi = + MockTestWebSettingsHostApi(); + TestWebSettingsHostApi.setup(mockWebSettingsPlatformHostApi); + + WebSettings.api = + WebSettingsHostApiImpl(instanceManager: instanceManager); + final int webSettingsInstanceId = + instanceManager.getInstanceId(webView.settings)!; + + webView.release(); + verify(mockWebSettingsPlatformHostApi.dispose(webSettingsInstanceId)); + verify(mockPlatformHostApi.dispose(webViewInstanceId)); + }); + }); + + group('WebSettings', () { + late MockTestWebSettingsHostApi mockPlatformHostApi; + + late InstanceManager instanceManager; + + late WebSettings webSettings; + late int webSettingsInstanceId; + + setUp(() { + instanceManager = InstanceManager(); + + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + WebView.api = WebViewHostApiImpl(instanceManager: instanceManager); + + mockPlatformHostApi = MockTestWebSettingsHostApi(); + TestWebSettingsHostApi.setup(mockPlatformHostApi); + + WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, + ); + + webSettings = WebSettings(WebView()); + webSettingsInstanceId = instanceManager.getInstanceId(webSettings)!; + }); + + test('create', () { + verify(mockPlatformHostApi.create(webSettingsInstanceId, any)); + }); + + test('setDomStorageEnabled', () { + webSettings.setDomStorageEnabled(false); + verify(mockPlatformHostApi.setDomStorageEnabled( + webSettingsInstanceId, + false, + )); + }); + + test('setJavaScriptCanOpenWindowsAutomatically', () { + webSettings.setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockPlatformHostApi.setJavaScriptCanOpenWindowsAutomatically( + webSettingsInstanceId, + true, + )); + }); + + test('setSupportMultipleWindows', () { + webSettings.setSupportMultipleWindows(false); + verify(mockPlatformHostApi.setSupportMultipleWindows( + webSettingsInstanceId, + false, + )); + }); + + test('setJavaScriptEnabled', () { + webSettings.setJavaScriptEnabled(true); + verify(mockPlatformHostApi.setJavaScriptEnabled( + webSettingsInstanceId, + true, + )); + }); + + test('setUserAgentString', () { + webSettings.setUserAgentString('hola'); + verify(mockPlatformHostApi.setUserAgentString( + webSettingsInstanceId, + 'hola', + )); + }); + + test('setMediaPlaybackRequiresUserGesture', () { + webSettings.setMediaPlaybackRequiresUserGesture(false); + verify(mockPlatformHostApi.setMediaPlaybackRequiresUserGesture( + webSettingsInstanceId, + false, + )); + }); + + test('setSupportZoom', () { + webSettings.setSupportZoom(true); + verify(mockPlatformHostApi.setSupportZoom( + webSettingsInstanceId, + true, + )); + }); + + test('setLoadWithOverviewMode', () { + webSettings.setLoadWithOverviewMode(false); + verify(mockPlatformHostApi.setLoadWithOverviewMode( + webSettingsInstanceId, + false, + )); + }); + + test('setUseWideViewPort', () { + webSettings.setUseWideViewPort(true); + verify(mockPlatformHostApi.setUseWideViewPort( + webSettingsInstanceId, + true, + )); + }); + + test('setDisplayZoomControls', () { + webSettings.setDisplayZoomControls(false); + verify(mockPlatformHostApi.setDisplayZoomControls( + webSettingsInstanceId, + false, + )); + }); + + test('setBuiltInZoomControls', () { + webSettings.setBuiltInZoomControls(true); + verify(mockPlatformHostApi.setBuiltInZoomControls( + webSettingsInstanceId, + true, + )); + }); + + test('setAllowFileAccess', () { + webSettings.setAllowFileAccess(true); + verify(mockPlatformHostApi.setAllowFileAccess( + webSettingsInstanceId, + true, + )); + }); + }); + + group('JavaScriptChannel', () { + late JavaScriptChannelFlutterApiImpl flutterApi; + + late InstanceManager instanceManager; + + late MockJavaScriptChannel mockJavaScriptChannel; + late int mockJavaScriptChannelInstanceId; + + setUp(() { + instanceManager = InstanceManager(); + flutterApi = JavaScriptChannelFlutterApiImpl( + instanceManager: instanceManager, + ); + + mockJavaScriptChannel = MockJavaScriptChannel(); + mockJavaScriptChannelInstanceId = + instanceManager.tryAddInstance(mockJavaScriptChannel)!; + }); + + test('postMessage', () { + flutterApi.postMessage( + mockJavaScriptChannelInstanceId, + 'Hello, World!', + ); + verify(mockJavaScriptChannel.postMessage('Hello, World!')); + }); + }); + + group('WebViewClient', () { + late WebViewClientFlutterApiImpl flutterApi; + + late InstanceManager instanceManager; + + late MockWebViewClient mockWebViewClient; + late int mockWebViewClientInstanceId; + + late MockWebView mockWebView; + late int mockWebViewInstanceId; + + setUp(() { + instanceManager = InstanceManager(); + flutterApi = WebViewClientFlutterApiImpl( + instanceManager: instanceManager, + ); + + mockWebViewClient = MockWebViewClient(); + mockWebViewClientInstanceId = + instanceManager.tryAddInstance(mockWebViewClient)!; + + mockWebView = MockWebView(); + mockWebViewInstanceId = instanceManager.tryAddInstance(mockWebView)!; + }); + + test('onPageStarted', () { + flutterApi.onPageStarted( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + 'https://www.google.com', + ); + verify(mockWebViewClient.onPageStarted( + mockWebView, + 'https://www.google.com', + )); + }); + + test('onPageFinished', () { + flutterApi.onPageFinished( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + 'https://www.google.com', + ); + verify(mockWebViewClient.onPageFinished( + mockWebView, + 'https://www.google.com', + )); + }); + + test('onReceivedRequestError', () { + flutterApi.onReceivedRequestError( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + WebResourceRequestData( + url: 'https://www.google.com', + isForMainFrame: true, + hasGesture: true, + method: 'POST', + isRedirect: false, + requestHeaders: {}, + ), + WebResourceErrorData(errorCode: 34, description: 'error description'), + ); + + verify(mockWebViewClient.onReceivedRequestError( + mockWebView, + argThat(isNotNull), + argThat(isNotNull), + )); + }); + + test('onReceivedError', () { + flutterApi.onReceivedError( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + 14, + 'desc', + 'https://www.google.com', + ); + + verify(mockWebViewClient.onReceivedError( + mockWebView, + 14, + 'desc', + 'https://www.google.com', + )); + }); + + test('requestLoading', () { + flutterApi.requestLoading( + mockWebViewClientInstanceId, + mockWebViewInstanceId, + WebResourceRequestData( + url: 'https://www.google.com', + isForMainFrame: true, + hasGesture: true, + method: 'POST', + isRedirect: true, + requestHeaders: {}, + ), + ); + + verify(mockWebViewClient.requestLoading( + mockWebView, + argThat(isNotNull), + )); + }); + + test('urlLoading', () { + flutterApi.urlLoading(mockWebViewClientInstanceId, + mockWebViewInstanceId, 'https://www.google.com'); + + verify(mockWebViewClient.urlLoading( + mockWebView, + 'https://www.google.com', + )); + }); + }); + + group('DownloadListener', () { + late DownloadListenerFlutterApiImpl flutterApi; + + late InstanceManager instanceManager; + + late MockDownloadListener mockDownloadListener; + late int mockDownloadListenerInstanceId; + + setUp(() { + instanceManager = InstanceManager(); + flutterApi = DownloadListenerFlutterApiImpl( + instanceManager: instanceManager, + ); + + mockDownloadListener = MockDownloadListener(); + mockDownloadListenerInstanceId = + instanceManager.tryAddInstance(mockDownloadListener)!; + }); + + test('onPageStarted', () { + flutterApi.onDownloadStart( + mockDownloadListenerInstanceId, + 'url', + 'userAgent', + 'contentDescription', + 'mimetype', + 45, + ); + verify(mockDownloadListener.onDownloadStart( + 'url', + 'userAgent', + 'contentDescription', + 'mimetype', + 45, + )); + }); + }); + + group('WebChromeClient', () { + late WebChromeClientFlutterApiImpl flutterApi; + + late InstanceManager instanceManager; + + late MockWebChromeClient mockWebChromeClient; + late int mockWebChromeClientInstanceId; + + late MockWebView mockWebView; + late int mockWebViewInstanceId; + + setUp(() { + instanceManager = InstanceManager(); + flutterApi = WebChromeClientFlutterApiImpl( + instanceManager: instanceManager, + ); + + mockWebChromeClient = MockWebChromeClient(); + mockWebChromeClientInstanceId = + instanceManager.tryAddInstance(mockWebChromeClient)!; + + mockWebView = MockWebView(); + mockWebViewInstanceId = instanceManager.tryAddInstance(mockWebView)!; + }); + + test('onPageStarted', () { + flutterApi.onProgressChanged( + mockWebChromeClientInstanceId, + mockWebViewInstanceId, + 76, + ); + verify(mockWebChromeClient.onProgressChanged(mockWebView, 76)); + }); + }); + }); + + group('CookieManager', () { + test('setCookie calls setCookie on CookieManagerHostApi', () { + CookieManager.api = MockCookieManagerHostApi(); + CookieManager.instance.setCookie('foo', 'bar'); + verify(CookieManager.api.setCookie('foo', 'bar')); + }); + + test('clearCookies calls clearCookies on CookieManagerHostApi', () { + CookieManager.api = MockCookieManagerHostApi(); + when(CookieManager.api.clearCookies()) + .thenAnswer((_) => Future.value(true)); + CookieManager.instance.clearCookies(); + 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.getInstanceId(webStorage)!; + }); + + test('create', () { + verify(mockPlatformHostApi.create(webStorageInstanceId)); + }); + + test('deleteAllData', () { + webStorage.deleteAllData(); + verify(mockPlatformHostApi.deleteAllData(webStorageInstanceId)); + }); + }); +} 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 new file mode 100644 index 000000000000..85ab6685ca34 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -0,0 +1,600 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_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; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/src/android_webview.pigeon.dart' as _i3; + +import 'test_android_webview.pigeon.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 +// 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 _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} + +/// A class which mocks [CookieManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManagerHostApi extends _i1.Mock + implements _i3.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); +} + +/// A class which mocks [DownloadListener]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDownloadListener extends _i1.Mock implements _i2.DownloadListener { + MockDownloadListener() { + _i1.throwOnMissingStub(this); + } + + @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); +} + +/// A class which mocks [JavaScriptChannel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { + MockJavaScriptChannel() { + _i1.throwOnMissingStub(this); + } + + @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); +} + +/// A class which mocks [TestDownloadListenerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestDownloadListenerHostApi extends _i1.Mock + implements _i5.TestDownloadListenerHostApi { + MockTestDownloadListenerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + 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 { + MockTestJavaScriptChannelHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + 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 { + MockTestWebChromeClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + 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 { + 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) => + super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setSupportMultipleWindows(int? instanceId, bool? support) => + 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) => + super.noSuchMethod( + Invocation.method(#setUserAgentString, [instanceId, userAgentString]), + returnValueForMissingStub: null); + @override + void setMediaPlaybackRequiresUserGesture(int? instanceId, bool? require) => + 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) => + 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); +} + +/// A class which mocks [TestWebStorageHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebStorageHostApi extends _i1.Mock + implements _i5.TestWebStorageHostApi { + MockTestWebStorageHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + 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 { + MockTestWebViewClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? shouldOverrideUrlLoading) => + super.noSuchMethod( + 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 { + MockTestWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + 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); + @override + void loadData( + 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) => + super.noSuchMethod( + 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])) 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 + _i4.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])) + 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 + void setWebContentsDebuggingEnabled(bool? enabled) => super.noSuchMethod( + Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), + returnValueForMissingStub: null); + @override + void setWebViewClient(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebViewClient, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + void addJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void removeJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + 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) => + 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); +} + +/// A class which mocks [TestAssetManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestAssetManagerHostApi extends _i1.Mock + implements _i5.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); +} + +/// A class which mocks [WebChromeClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { + MockWebChromeClient() { + _i1.throwOnMissingStub(this); + } + + @override + void onProgressChanged(_i2.WebView? webView, int? progress) => super + .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), + returnValueForMissingStub: null); +} + +/// A class which mocks [WebView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebView extends _i1.Mock implements _i2.WebView { + MockWebView() { + _i1.throwOnMissingStub(this); + } + + @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}) => + (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}) => + (super.noSuchMethod( + 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) => + (super.noSuchMethod( + Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.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(_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); +} + +/// A class which mocks [WebViewClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { + MockWebViewClient() { + _i1.throwOnMissingStub(this); + } + + @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 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); +} 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 new file mode 100644 index 000000000000..3aeb005efb86 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.dart @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors. 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_android/src/instance_manager.dart'; + +void main() { + group('InstanceManager', () { + late InstanceManager testInstanceManager; + + setUp(() { + testInstanceManager = InstanceManager(); + }); + + test('tryAddInstance', () { + final Object object = Object(); + + expect(testInstanceManager.tryAddInstance(object), 0); + expect(testInstanceManager.getInstanceId(object), 0); + expect(testInstanceManager.getInstance(0), object); + expect(testInstanceManager.tryAddInstance(object), null); + }); + + test('removeInstance', () { + final Object object = Object(); + testInstanceManager.tryAddInstance(object); + + expect(testInstanceManager.removeInstance(object), 0); + expect(testInstanceManager.getInstanceId(object), null); + expect(testInstanceManager.getInstance(0), null); + expect(testInstanceManager.removeInstance(object), null); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart new file mode 100644 index 000000000000..e3c5909f3207 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart @@ -0,0 +1,1193 @@ +// Copyright 2013 The Flutter 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: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_android/src/android_webview.pigeon.dart'; + +class _TestWebViewHostApiCodec extends StandardMessageCodec { + const _TestWebViewHostApiCodec(); +} + +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 loadUrl(int instanceId, String url, Map headers); + void postUrl(int instanceId, String url, Uint8List data); + String? getUrl(int instanceId); + bool canGoBack(int instanceId); + bool canGoForward(int instanceId); + void goBack(int instanceId); + void goForward(int instanceId); + void reload(int instanceId); + void clearCache(int instanceId, bool includeDiskFiles); + Future evaluateJavascript(int instanceId, String javascriptString); + String? getTitle(int instanceId); + void scrollTo(int instanceId, int x, int y); + void scrollBy(int instanceId, int x, int y); + int getScrollX(int instanceId); + int getScrollY(int instanceId); + void setWebContentsDebuggingEnabled(bool enabled); + void setWebViewClient(int instanceId, int webViewClientInstanceId); + void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + void setDownloadListener(int instanceId, int? listenerInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); + void setBackgroundColor(int instanceId, int color); + static void setup(TestWebViewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.create(arg_instanceId!, arg_useHybridComposition!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + final String? arg_mimeType = (args[2] as String?); + final String? arg_encoding = (args[3] as String?); + api.loadData(arg_instanceId!, arg_data!, arg_mimeType, arg_encoding); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final String? arg_baseUrl = (args[1] as 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.'); + final String? arg_mimeType = (args[3] as String?); + final String? arg_encoding = (args[4] as String?); + final String? arg_historyUrl = (args[5] as String?); + 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); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + 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.'); + 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.'); + api.loadUrl(arg_instanceId!, arg_url!, arg_headers!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + api.postUrl(arg_instanceId!, arg_url!, arg_data!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final bool output = api.canGoBack(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final bool output = api.canGoForward(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.goBack(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.goForward(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.reload(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.clearCache(arg_instanceId!, arg_includeDiskFiles!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + api.scrollTo(arg_instanceId!, arg_x!, arg_y!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + 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.'); + api.scrollBy(arg_instanceId!, arg_x!, arg_y!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final int output = api.getScrollX(arg_instanceId!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final int output = api.getScrollY(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.'); + 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.'); + api.setWebContentsDebuggingEnabled(arg_enabled!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setWebViewClient(arg_instanceId!, arg_webViewClientInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final int? arg_listenerInstanceId = (args[1] as int?); + api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final int? arg_clientInstanceId = (args[1] as int?); + api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setBackgroundColor(arg_instanceId!, arg_color!); + return {}; + }); + } + } + } +} + +class _TestWebSettingsHostApiCodec extends StandardMessageCodec { + const _TestWebSettingsHostApiCodec(); +} + +abstract class TestWebSettingsHostApi { + static const MessageCodec codec = _TestWebSettingsHostApiCodec(); + + void create(int instanceId, int webViewInstanceId); + void dispose(int instanceId); + void setDomStorageEnabled(int instanceId, bool flag); + void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag); + void setSupportMultipleWindows(int instanceId, bool support); + void setJavaScriptEnabled(int instanceId, bool flag); + void setUserAgentString(int instanceId, String? userAgentString); + void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); + void setSupportZoom(int instanceId, bool support); + void setLoadWithOverviewMode(int instanceId, bool overview); + void setUseWideViewPort(int instanceId, bool use); + void setDisplayZoomControls(int instanceId, bool enabled); + void setBuiltInZoomControls(int instanceId, bool enabled); + void setAllowFileAccess(int instanceId, bool enabled); + static void setup(TestWebSettingsHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.create(arg_instanceId!, arg_webViewInstanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.dispose(arg_instanceId!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setDomStorageEnabled(arg_instanceId!, arg_flag!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setSupportMultipleWindows(arg_instanceId!, arg_support!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setJavaScriptEnabled(arg_instanceId!, arg_flag!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final String? arg_userAgentString = (args[1] as String?); + api.setUserAgentString(arg_instanceId!, arg_userAgentString); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setSupportZoom(arg_instanceId!, arg_support!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setLoadWithOverviewMode(arg_instanceId!, arg_overview!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setUseWideViewPort(arg_instanceId!, arg_use!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setDisplayZoomControls(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setBuiltInZoomControls(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.setAllowFileAccess(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } + } +} + +class _TestJavaScriptChannelHostApiCodec extends StandardMessageCodec { + const _TestJavaScriptChannelHostApiCodec(); +} + +abstract class TestJavaScriptChannelHostApi { + static const MessageCodec codec = + _TestJavaScriptChannelHostApiCodec(); + + void create(int instanceId, String channelName); + static void setup(TestJavaScriptChannelHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.create(arg_instanceId!, arg_channelName!); + return {}; + }); + } + } + } +} + +class _TestWebViewClientHostApiCodec extends StandardMessageCodec { + const _TestWebViewClientHostApiCodec(); +} + +abstract class TestWebViewClientHostApi { + static const MessageCodec codec = _TestWebViewClientHostApiCodec(); + + void create(int instanceId, bool shouldOverrideUrlLoading); + static void setup(TestWebViewClientHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.create(arg_instanceId!, arg_shouldOverrideUrlLoading!); + return {}; + }); + } + } + } +} + +class _TestDownloadListenerHostApiCodec extends StandardMessageCodec { + const _TestDownloadListenerHostApiCodec(); +} + +abstract class TestDownloadListenerHostApi { + static const MessageCodec codec = + _TestDownloadListenerHostApiCodec(); + + void create(int instanceId); + static void setup(TestDownloadListenerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + api.create(arg_instanceId!); + return {}; + }); + } + } + } +} + +class _TestWebChromeClientHostApiCodec extends StandardMessageCodec { + const _TestWebChromeClientHostApiCodec(); +} + +abstract class TestWebChromeClientHostApi { + static const MessageCodec codec = _TestWebChromeClientHostApiCodec(); + + void create(int instanceId, int webViewClientInstanceId); + static void setup(TestWebChromeClientHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + 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.'); + api.create(arg_instanceId!, arg_webViewClientInstanceId!); + return {}; + }); + } + } + } +} + +class _TestAssetManagerHostApiCodec extends StandardMessageCodec { + const _TestAssetManagerHostApiCodec(); +} + +abstract class TestAssetManagerHostApi { + static const MessageCodec codec = _TestAssetManagerHostApiCodec(); + + List list(String path); + String getAssetFilePathByName(String name); + static void setup(TestAssetManagerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final List output = api.list(arg_path!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + '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.'); + 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.'); + final String output = api.getAssetFilePathByName(arg_name!); + return {'result': output}; + }); + } + } + } +} + +class _TestWebStorageHostApiCodec extends StandardMessageCodec { + const _TestWebStorageHostApiCodec(); +} + +abstract class TestWebStorageHostApi { + static const MessageCodec codec = _TestWebStorageHostApiCodec(); + + 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.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart new file mode 100644 index 000000000000..4f274ff4499f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_android_cookie_manager_test.mocks.dart'; + +@GenerateMocks([android_webview.CookieManager]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + android_webview.CookieManager.instance = MockCookieManager(); + }); + + test('clearCookies should call android_webview.clearCookies', () { + when(android_webview.CookieManager.instance.clearCookies()) + .thenAnswer((_) => Future.value(true)); + WebViewAndroidCookieManager().clearCookies(); + verify(android_webview.CookieManager.instance.clearCookies()); + }); + + test('setCookie should throw ArgumentError for cookie with invalid path', () { + expect( + () => WebViewAndroidCookieManager().setCookie(const WebViewCookie( + name: 'foo', + value: 'bar', + domain: 'flutter.dev', + path: 'invalid;path', + )), + throwsA(const TypeMatcher()), + ); + }); + + test( + 'setCookie should call android_webview.csetCookie with properly formatted cookie value', + () { + WebViewAndroidCookieManager().setCookie(const WebViewCookie( + name: 'foo&', + value: 'bar@', + domain: 'flutter.dev', + )); + verify(android_webview.CookieManager.instance + .setCookie('flutter.dev', 'foo%26=bar%40; path=/')); + }); +} 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 new file mode 100644 index 000000000000..ff9974b1a10b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart @@ -0,0 +1,37 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_android/test/webview_android_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_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 +// 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 [CookieManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManager extends _i1.Mock implements _i2.CookieManager { + MockCookieManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(String? url, String? value) => + (super.noSuchMethod(Invocation.method(#setCookie, [url, value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: 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 new file mode 100644 index 000000000000..2432b35a4814 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart @@ -0,0 +1,1036 @@ +// Copyright 2013 The Flutter Authors. 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 '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_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; +import 'package:webview_flutter_android/webview_android_widget.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.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, + WebViewAndroidWebViewClient, + JavascriptChannelRegistry, + WebViewPlatformCallbacksHandler, + WebViewProxy, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebViewAndroidWidget', () { + late MockFlutterAssetManager mockFlutterAssetManager; + late MockWebView mockWebView; + late MockWebSettings mockWebSettings; + late MockWebStorage mockWebStorage; + late MockWebViewProxy mockWebViewProxy; + + late MockWebViewPlatformCallbacksHandler mockCallbacksHandler; + late WebViewAndroidWebViewClient webViewClient; + late WebViewAndroidDownloadListener downloadListener; + late WebViewAndroidWebChromeClient webChromeClient; + + late MockJavascriptChannelRegistry mockJavascriptChannelRegistry; + + late WebViewAndroidPlatformController testController; + + setUp(() { + mockFlutterAssetManager = MockFlutterAssetManager(); + mockWebView = MockWebView(); + mockWebSettings = MockWebSettings(); + mockWebStorage = MockWebStorage(); + when(mockWebView.settings).thenReturn(mockWebSettings); + + mockWebViewProxy = MockWebViewProxy(); + when(mockWebViewProxy.createWebView( + useHybridComposition: anyNamed('useHybridComposition'), + )).thenReturn(mockWebView); + + mockCallbacksHandler = MockWebViewPlatformCallbacksHandler(); + mockJavascriptChannelRegistry = MockJavascriptChannelRegistry(); + }); + + // Builds a AndroidWebViewWidget with default parameters. + Future buildWidget( + WidgetTester tester, { + CreationParams? creationParams, + bool hasNavigationDelegate = false, + bool hasProgressTracking = false, + bool useHybridComposition = false, + }) async { + await tester.pumpWidget(WebViewAndroidWidget( + useHybridComposition: useHybridComposition, + creationParams: creationParams ?? + CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + )), + callbacksHandler: mockCallbacksHandler, + javascriptChannelRegistry: mockJavascriptChannelRegistry, + webViewProxy: mockWebViewProxy, + flutterAssetManager: mockFlutterAssetManager, + webStorage: mockWebStorage, + onBuildWidget: (WebViewAndroidPlatformController controller) { + testController = controller; + return Container(); + }, + )); + + webViewClient = testController.webViewClient; + downloadListener = testController.downloadListener; + webChromeClient = testController.webChromeClient; + } + + testWidgets('WebViewAndroidWidget', (WidgetTester tester) async { + await buildWidget(tester); + + verify(mockWebSettings.setDomStorageEnabled(true)); + verify(mockWebSettings.setJavaScriptCanOpenWindowsAutomatically(true)); + verify(mockWebSettings.setSupportMultipleWindows(true)); + verify(mockWebSettings.setLoadWithOverviewMode(true)); + verify(mockWebSettings.setUseWideViewPort(true)); + verify(mockWebSettings.setDisplayZoomControls(false)); + verify(mockWebSettings.setBuiltInZoomControls(true)); + + verifyInOrder(>[ + mockWebView.setWebViewClient(webViewClient), + mockWebView.setDownloadListener(downloadListener), + mockWebView.setWebChromeClient(webChromeClient), + ]); + }); + + testWidgets( + 'Create Widget with Hybrid Composition', + (WidgetTester tester) async { + await buildWidget(tester, useHybridComposition: true); + verify(mockWebViewProxy.createWebView(useHybridComposition: true)); + }, + ); + + 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, + ), + ), + ); + verify(mockWebView.loadUrl( + 'https://www.google.com', + {}, + )); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + userAgent: 'MyUserAgent', + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebSettings.setUserAgentString('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, + ), + ), + ); + + verify(mockWebSettings.setMediaPlaybackRequiresUserGesture(any)); + }); + + testWidgets('autoMediaPlaybackPolicy false', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + autoMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebSettings.setMediaPlaybackRequiresUserGesture(false)); + }); + + testWidgets('javascriptChannelNames', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + javascriptChannelNames: {'a', 'b'}, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + final List javaScriptChannels = + verify(mockWebView.addJavaScriptChannel(captureAny)).captured; + expect(javaScriptChannels[0].channelName, 'a'); + expect(javaScriptChannels[1].channelName, 'b'); + }); + + group('WebSettings', () { + testWidgets('javascriptMode', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebSettings.setJavaScriptEnabled(true)); + }); + + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: true, + ), + ), + ); + + expect(testController.webViewClient.handlesNavigation, isTrue); + expect(testController.webViewClient.shouldOverrideUrlLoading, isTrue); + }); + + testWidgets('debuggingEnabled true', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + debuggingEnabled: true, + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebViewProxy.setWebContentsDebuggingEnabled(true)); + }); + + testWidgets('debuggingEnabled false', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + debuggingEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebViewProxy.setWebContentsDebuggingEnabled(false)); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.of('myUserAgent'), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebSettings.setUserAgentString('myUserAgent')); + }); + + testWidgets('zoomEnabled', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + zoomEnabled: false, + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebSettings.setSupportZoom(false)); + }); + }); + }); + + group('WebViewPlatformController', () { + testWidgets('loadFile without "file://" prefix', + (WidgetTester tester) async { + await buildWidget(tester); + + const String filePath = '/path/to/file.html'; + await testController.loadFile(filePath); + + verify(mockWebView.loadUrl( + 'file://$filePath', + {}, + )); + }); + + testWidgets('loadFile with "file://" prefix', + (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFile('file:///path/to/file.html'); + + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )); + }); + + testWidgets('loadFile should setAllowFileAccess to true', + (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFile('file:///path/to/file.html'); + + verify(mockWebSettings.setAllowFileAccess(true)); + }); + + testWidgets('loadFlutterAsset', (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'test_assets/index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets/test_assets')) + .thenAnswer( + (_) => Future>.value(['index.html'])); + + await testController.loadFlutterAsset(assetKey); + + verify(mockWebView.loadUrl( + 'file:///android_asset/flutter_assets/$assetKey', + {}, + )); + }); + + testWidgets('loadFlutterAsset with file in root', + (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets')).thenAnswer( + (_) => Future>.value(['index.html'])); + + await testController.loadFlutterAsset(assetKey); + + verify(mockWebView.loadUrl( + 'file:///android_asset/flutter_assets/$assetKey', + {}, + )); + }); + + testWidgets( + 'loadFlutterAsset throws ArgumentError when asset does not exists', + (WidgetTester tester) async { + await buildWidget(tester); + const String assetKey = 'test_assets/index.html'; + + when(mockFlutterAssetManager.getAssetFilePathByName(assetKey)) + .thenAnswer( + (_) => Future.value('flutter_assets/$assetKey')); + when(mockFlutterAssetManager.list('flutter_assets/test_assets')) + .thenAnswer((_) => Future>.value([''])); + + expect( + () => testController.loadFlutterAsset(assetKey), + throwsA( + isA() + .having((ArgumentError error) => error.name, 'name', 'key') + .having((ArgumentError error) => error.message, 'message', + 'Asset for key "$assetKey" not found.'), + ), + ); + }); + + testWidgets('loadHtmlString without base URL', + (WidgetTester tester) async { + await buildWidget(tester); + + const String htmlString = 'Test data.'; + await testController.loadHtmlString(htmlString); + + verify(mockWebView.loadDataWithBaseUrl( + data: htmlString, + mimeType: 'text/html', + )); + }); + + testWidgets('loadHtmlString with base URL', (WidgetTester tester) async { + await buildWidget(tester); + + const String htmlString = 'Test data.'; + await testController.loadHtmlString( + htmlString, + baseUrl: 'https://flutter.dev', + ); + + verify(mockWebView.loadDataWithBaseUrl( + baseUrl: 'https://flutter.dev', + data: htmlString, + mimeType: 'text/html', + )); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadUrl( + 'https://www.google.com', + {'a': 'header'}, + ); + + verify(mockWebView.loadUrl( + 'https://www.google.com', + {'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, + )); + + verify(mockWebView.loadUrl( + 'https://www.google.com', + {}, + )); + }); + + 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'}, + )); + + verify(mockWebView.loadUrl( + 'https://www.google.com', + {'a': 'header'}, + )); + }); + + testWidgets('POST without body', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + )); + + verify(mockWebView.postUrl( + 'https://www.google.com', + Uint8List(0), + )); + }); + + testWidgets('POST with body', (WidgetTester tester) async { + await buildWidget(tester); + + final Uint8List body = Uint8List.fromList('Test Body'.codeUnits); + + await testController.loadRequest(WebViewRequest( + uri: Uri.parse('https://www.google.com'), + method: WebViewRequestMethod.post, + body: body)); + + verify(mockWebView.postUrl( + 'https://www.google.com', + body, + )); + }); + }); + + testWidgets('no update to userAgentString when there is no change', + (WidgetTester tester) async { + await buildWidget(tester); + + reset(mockWebSettings); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + )); + + verifyNever(mockWebSettings.setUserAgentString(any)); + }); + + testWidgets('update null userAgentString with empty string', + (WidgetTester tester) async { + await buildWidget(tester); + + reset(mockWebSettings); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.of(null), + )); + + verify(mockWebSettings.setUserAgentString('')); + }); + + testWidgets('currentUrl', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getUrl()) + .thenAnswer((_) => Future.value('https://www.google.com')); + expect( + testController.currentUrl(), completion('https://www.google.com')); + }); + + 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('clearCache', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.clearCache(); + verify(mockWebView.clearCache(true)); + verify(mockWebStorage.deleteAllData()); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavascript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.evaluateJavascript('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets('runJavascriptReturningResult', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavascript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.runJavascriptReturningResult('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets('runJavascript', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.evaluateJavascript('runJavaScript')).thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + testController.runJavascript('runJavaScript'), + completes, + ); + }); + + testWidgets('addJavascriptChannels', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.addJavascriptChannels({'c', 'd'}); + final List javaScriptChannels = + verify(mockWebView.addJavaScriptChannel(captureAny)).captured; + expect(javaScriptChannels[0].channelName, 'c'); + expect(javaScriptChannels[1].channelName, 'd'); + }); + + testWidgets('removeJavascriptChannels', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.addJavascriptChannels({'c', 'd'}); + await testController.removeJavascriptChannels({'c', 'd'}); + final List javaScriptChannels = + verify(mockWebView.removeJavaScriptChannel(captureAny)).captured; + expect(javaScriptChannels[0].channelName, 'c'); + expect(javaScriptChannels[1].channelName, 'd'); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getTitle()) + .thenAnswer((_) => Future.value('Web Title')); + expect(testController.getTitle(), completion('Web Title')); + }); + + testWidgets('scrollTo', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.scrollTo(1, 2); + verify(mockWebView.scrollTo(1, 2)); + }); + + testWidgets('scrollBy', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.scrollBy(3, 4); + verify(mockWebView.scrollBy(3, 4)); + }); + + testWidgets('getScrollX', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getScrollX()).thenAnswer((_) => Future.value(23)); + expect(testController.getScrollX(), completion(23)); + }); + + testWidgets('getScrollY', (WidgetTester tester) async { + await buildWidget(tester); + + when(mockWebView.getScrollY()).thenAnswer((_) => Future.value(25)); + expect(testController.getScrollY(), completion(25)); + }); + }); + + group('WebViewPlatformCallbacksHandler', () { + testWidgets('onPageStarted', (WidgetTester tester) async { + await buildWidget(tester); + webViewClient.onPageStarted(mockWebView, 'https://google.com'); + verify(mockCallbacksHandler.onPageStarted('https://google.com')); + }); + + testWidgets('onPageFinished', (WidgetTester tester) async { + await buildWidget(tester); + webViewClient.onPageFinished(mockWebView, 'https://google.com'); + verify(mockCallbacksHandler.onPageFinished('https://google.com')); + }); + + testWidgets('onWebResourceError from onReceivedError', + (WidgetTester tester) async { + await buildWidget(tester); + webViewClient.onReceivedError( + mockWebView, + android_webview.WebViewClient.errorAuthentication, + 'description', + 'https://google.com', + ); + + final WebResourceError error = + verify(mockCallbacksHandler.onWebResourceError(captureAny)) + .captured + .single as WebResourceError; + expect(error.description, 'description'); + expect(error.errorCode, -4); + expect(error.failingUrl, 'https://google.com'); + expect(error.domain, isNull); + expect(error.errorType, WebResourceErrorType.authentication); + }); + + testWidgets('onWebResourceError from onReceivedRequestError', + (WidgetTester tester) async { + await buildWidget(tester); + webViewClient.onReceivedRequestError( + mockWebView, + android_webview.WebResourceRequest( + url: 'https://google.com', + isForMainFrame: true, + isRedirect: false, + hasGesture: false, + method: 'POST', + requestHeaders: {}, + ), + android_webview.WebResourceError( + errorCode: android_webview.WebViewClient.errorUnsafeResource, + description: 'description', + ), + ); + + final WebResourceError error = + verify(mockCallbacksHandler.onWebResourceError(captureAny)) + .captured + .single as WebResourceError; + expect(error.description, 'description'); + expect(error.errorCode, -16); + expect(error.failingUrl, 'https://google.com'); + expect(error.domain, isNull); + expect(error.errorType, WebResourceErrorType.unsafeResource); + }); + + testWidgets('onNavigationRequest from urlLoading', + (WidgetTester tester) async { + await buildWidget(tester, hasNavigationDelegate: true); + when(mockCallbacksHandler.onNavigationRequest( + isForMainFrame: argThat(isTrue, named: 'isForMainFrame'), + url: 'https://google.com', + )).thenReturn(true); + + webViewClient.urlLoading(mockWebView, 'https://google.com'); + verify(mockCallbacksHandler.onNavigationRequest( + url: 'https://google.com', + isForMainFrame: true, + )); + verify(mockWebView.loadUrl('https://google.com', {})); + }); + + testWidgets('onNavigationRequest from requestLoading', + (WidgetTester tester) async { + await buildWidget(tester, hasNavigationDelegate: true); + when(mockCallbacksHandler.onNavigationRequest( + isForMainFrame: argThat(isTrue, named: 'isForMainFrame'), + url: 'https://google.com', + )).thenReturn(true); + + webViewClient.requestLoading( + mockWebView, + android_webview.WebResourceRequest( + url: 'https://google.com', + isForMainFrame: true, + isRedirect: false, + hasGesture: false, + method: 'POST', + requestHeaders: {}, + ), + ); + verify(mockCallbacksHandler.onNavigationRequest( + url: 'https://google.com', + isForMainFrame: true, + )); + verify(mockWebView.loadUrl('https://google.com', {})); + }); + + group('JavascriptChannelRegistry', () { + testWidgets('onJavascriptChannelMessage', (WidgetTester tester) async { + await buildWidget(tester); + + await testController.addJavascriptChannels({'hello'}); + + final WebViewAndroidJavaScriptChannel javaScriptChannel = + verify(mockWebView.addJavaScriptChannel(captureAny)) + .captured + .single as WebViewAndroidJavaScriptChannel; + javaScriptChannel.postMessage('goodbye'); + verify(mockJavascriptChannelRegistry.onJavascriptChannelMessage( + 'hello', + 'goodbye', + )); + }); + }); + }); + }); + + group('WebViewProxy', () { + late MockTestWebViewHostApi mockPlatformHostApi; + late InstanceManager instanceManager; + + setUp(() { + // WebViewProxy calls static methods that can't be mocked, so the mocks + // have to be set up at the next layer down, by mocking the implementation + // of WebView itstelf. + mockPlatformHostApi = MockTestWebViewHostApi(); + TestWebViewHostApi.setup(mockPlatformHostApi); + instanceManager = InstanceManager(); + android_webview.WebView.api = + WebViewHostApiImpl(instanceManager: instanceManager); + }); + + test('setWebContentsDebuggingEnabled true', () { + const WebViewProxy webViewProxy = WebViewProxy(); + webViewProxy.setWebContentsDebuggingEnabled(true); + verify(mockPlatformHostApi.setWebContentsDebuggingEnabled(true)); + }); + + test('setWebContentsDebuggingEnabled false', () { + const WebViewProxy webViewProxy = WebViewProxy(); + webViewProxy.setWebContentsDebuggingEnabled(false); + 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 new file mode 100644 index 000000000000..78e60cac1b8e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart @@ -0,0 +1,529 @@ +// Mocks generated by Mockito 5.1.0 from annotations +// in webview_flutter_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; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/webview_android_widget.dart' as _i7; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} + +class _FakeJavascriptChannelRegistry_1 extends _i1.Fake + implements _i3.JavascriptChannelRegistry {} + +class _FakeWebView_2 extends _i1.Fake implements _i2.WebView {} + +/// A class which mocks [FlutterAssetManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterAssetManager extends _i1.Mock + implements _i2.FlutterAssetManager { + MockFlutterAssetManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future> list(String? path) => + (super.noSuchMethod(Invocation.method(#list, [path]), + returnValue: Future>.value([])) + as _i4.Future>); + @override + _i4.Future getAssetFilePathByName(String? name) => + (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), + returnValue: Future.value('')) as _i4.Future); +} + +/// A class which mocks [WebSettings]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebSettings extends _i1.Mock implements _i2.WebSettings { + MockWebSettings() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future setDomStorageEnabled(bool? flag) => + (super.noSuchMethod(Invocation.method(#setDomStorageEnabled, [flag]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setJavaScriptCanOpenWindowsAutomatically(bool? flag) => + (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) => + (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); +} + +/// 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 + _i4.Future deleteAllData() => + (super.noSuchMethod(Invocation.method(#deleteAllData, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); +} + +/// A class which mocks [WebView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebView extends _i1.Mock implements _i2.WebView { + MockWebView() { + _i1.throwOnMissingStub(this); + } + + @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}) => + (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}) => + (super.noSuchMethod( + 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) => + (super.noSuchMethod( + Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.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); +} + +/// 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]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewAndroidDownloadListener extends _i1.Mock + implements _i7.WebViewAndroidDownloadListener { + MockWebViewAndroidDownloadListener() { + _i1.throwOnMissingStub(this); + } + + @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) => + super.noSuchMethod( + Invocation.method(#onDownloadStart, + [url, userAgent, contentDisposition, mimetype, contentLength]), + returnValueForMissingStub: null); +} + +/// A class which mocks [WebViewAndroidJavaScriptChannel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewAndroidJavaScriptChannel extends _i1.Mock + implements _i7.WebViewAndroidJavaScriptChannel { + MockWebViewAndroidJavaScriptChannel() { + _i1.throwOnMissingStub(this); + } + + @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); +} + +/// A class which mocks [WebViewAndroidWebChromeClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewAndroidWebChromeClient extends _i1.Mock + implements _i7.WebViewAndroidWebChromeClient { + MockWebViewAndroidWebChromeClient() { + _i1.throwOnMissingStub(this); + } + + @override + void onProgressChanged(_i2.WebView? webView, int? progress) => super + .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), + returnValueForMissingStub: null); +} + +/// A class which mocks [WebViewAndroidWebViewClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewAndroidWebViewClient extends _i1.Mock + implements _i7.WebViewAndroidWebViewClient { + MockWebViewAndroidWebViewClient() { + _i1.throwOnMissingStub(this); + } + + @override + 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)); + @override + void Function(_i3.WebResourceError) get onWebResourceErrorCallback => + (super.noSuchMethod(Invocation.getter(#onWebResourceErrorCallback), + returnValue: (_i3.WebResourceError error) {}) + as void Function(_i3.WebResourceError)); + @override + set onWebResourceErrorCallback( + void Function(_i3.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) => + 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); +} + +/// A class which mocks [JavascriptChannelRegistry]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavascriptChannelRegistry extends _i1.Mock + implements _i3.JavascriptChannelRegistry { + MockJavascriptChannelRegistry() { + _i1.throwOnMissingStub(this); + } + + @override + Map get channels => + (super.noSuchMethod(Invocation.getter(#channels), + returnValue: {}) + as Map); + @override + void onJavascriptChannelMessage(String? channel, String? message) => + super.noSuchMethod( + Invocation.method(#onJavascriptChannelMessage, [channel, message]), + returnValueForMissingStub: null); + @override + void updateJavascriptChannelsFromSet(Set<_i3.JavascriptChannel>? channels) => + super.noSuchMethod( + 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 { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.FutureOr onNavigationRequest({String? url, 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); +} + +/// A class which mocks [WebViewProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewProxy extends _i1.Mock implements _i7.WebViewProxy { + MockWebViewProxy() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WebView createWebView({bool? useHybridComposition}) => + (super.noSuchMethod( + Invocation.method(#createWebView, [], + {#useHybridComposition: useHybridComposition}), + returnValue: _FakeWebView_2()) as _i2.WebView); + @override + _i4.Future setWebContentsDebuggingEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 9e217a04e961..565bfee21186 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,72 @@ +## NEXT + +* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). + +## 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 + +* Update to use the `verify` method introduced in platform_plugin_interface 2.1.0. + +## 1.8.0 + +* Adds the `loadFlutterAsset` method to the platform interface. + +## 1.7.0 + +* Add an option to set the background color of the webview. + +## 1.6.1 + +* Revert deprecation of `clearCookies` in WebViewPlatform for later deprecation. + +## 1.6.0 + +* Adds platform interface for cookie manager. +* Deprecates `clearCookies` in WebViewPlatform in favour of `CookieManager#clearCookies`. +* Expanded `CreationParams` to include cookies to be set at webview creation. + +## 1.5.2 + +* Mirgrates from analysis_options_legacy.yaml to the more strict analysis_options.yaml. + +## 1.5.1 + +* Reverts the addition of `onUrlChanged`, which was unintentionally a breaking + change. + +## 1.5.0 + +* Added `onUrlChanged` callback to platform callback handler. + +## 1.4.0 + +* Added `loadFile` and `loadHtml` interface methods. + +## 1.3.0 + +* Added `loadRequest` method to platform interface. + +## 1.2.0 + +* Added `runJavascript` and `runJavascriptReturningResult` interface methods to supersede `evaluateJavascript`. + +## 1.1.0 + +* Add `zoomEnabled` functionality to `WebSettings`. + ## 1.0.0 -* Extracted platform interface from `webview_flutter`. \ No newline at end of file +* Extracted platform interface from `webview_flutter`. 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 b467daf72a08..f32881701cfb 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'; @@ -35,32 +34,34 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { Future _onMethodCall(MethodCall call) async { switch (call.method) { case 'javascriptChannelMessage': - final String channel = call.arguments['channel']!; - final String message = call.arguments['message']!; + final String channel = call.arguments['channel']! as String; + final String message = call.arguments['message']! as String; _javascriptChannelRegistry.onJavascriptChannelMessage(channel, message); return true; case 'navigationRequest': return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url']!, - isForMainFrame: call.arguments['isForMainFrame']!, + url: call.arguments['url']! as String, + isForMainFrame: call.arguments['isForMainFrame']! as bool, ); case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']!); + _platformCallbacksHandler + .onPageFinished(call.arguments['url']! as String); return null; case 'onProgress': - _platformCallbacksHandler.onProgress(call.arguments['progress']); + _platformCallbacksHandler.onProgress(call.arguments['progress'] as int); return null; case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']!); + _platformCallbacksHandler + .onPageStarted(call.arguments['url']! as String); return null; case 'onWebResourceError': _platformCallbacksHandler.onWebResourceError( WebResourceError( - errorCode: call.arguments['errorCode']!, - description: call.arguments['description']!, + errorCode: call.arguments['errorCode']! as int, + description: call.arguments['description']! as String, // iOS doesn't support `failingUrl`. - failingUrl: call.arguments['failingUrl'], - domain: call.arguments['domain'], + failingUrl: call.arguments['failingUrl'] as String?, + domain: call.arguments['domain'] as String?, errorType: call.arguments['errorType'] == null ? null : WebResourceErrorType.values.firstWhere( @@ -79,6 +80,48 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { ); } + @override + Future loadFile(String absoluteFilePath) async { + assert(absoluteFilePath != null); + + try { + return await _channel.invokeMethod('loadFile', absoluteFilePath); + } on PlatformException catch (ex) { + if (ex.code == 'loadFile_failed') { + throw ArgumentError(ex.message); + } + + rethrow; + } + } + + @override + Future loadFlutterAsset(String key) async { + assert(key.isNotEmpty); + + try { + return await _channel.invokeMethod('loadFlutterAsset', key); + } on PlatformException catch (ex) { + if (ex.code == 'loadFlutterAsset_invalidKey') { + throw ArgumentError(ex.message); + } + + rethrow; + } + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) async { + assert(html != null); + return _channel.invokeMethod('loadHtmlString', { + 'html': html, + 'baseUrl': baseUrl, + }); + } + @override Future loadUrl( String url, @@ -91,28 +134,37 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { }); } + @override + Future loadRequest(WebViewRequest request) async { + assert(request != null); + return _channel.invokeMethod('loadRequest', { + 'request': request.toJson(), + }); + } + @override Future currentUrl() => _channel.invokeMethod('currentUrl'); @override Future canGoBack() => - _channel.invokeMethod("canGoBack").then((result) => result!); + _channel.invokeMethod('canGoBack').then((bool? result) => result!); @override - Future canGoForward() => - _channel.invokeMethod("canGoForward").then((result) => result!); + Future canGoForward() => _channel + .invokeMethod('canGoForward') + .then((bool? result) => result!); @override - Future goBack() => _channel.invokeMethod("goBack"); + Future goBack() => _channel.invokeMethod('goBack'); @override - Future goForward() => _channel.invokeMethod("goForward"); + Future goForward() => _channel.invokeMethod('goForward'); @override - Future reload() => _channel.invokeMethod("reload"); + Future reload() => _channel.invokeMethod('reload'); @override - Future clearCache() => _channel.invokeMethod("clearCache"); + Future clearCache() => _channel.invokeMethod('clearCache'); @override Future updateSettings(WebSettings settings) async { @@ -123,10 +175,22 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future evaluateJavascript(String javascriptString) { + Future evaluateJavascript(String javascript) { + return _channel + .invokeMethod('evaluateJavascript', javascript) + .then((String? result) => result!); + } + + @override + Future runJavascript(String javascript) async { + await _channel.invokeMethod('runJavascript', javascript); + } + + @override + Future runJavascriptReturningResult(String javascript) { return _channel - .invokeMethod('evaluateJavascript', javascriptString) - .then((result) => result!); + .invokeMethod('runJavascriptReturningResult', javascript) + .then((String? result) => result!); } @override @@ -142,7 +206,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future getTitle() => _channel.invokeMethod("getTitle"); + Future getTitle() => _channel.invokeMethod('getTitle'); @override Future scrollTo(int x, int y) { @@ -162,17 +226,23 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { @override Future getScrollX() => - _channel.invokeMethod("getScrollX").then((result) => result!); + _channel.invokeMethod('getScrollX').then((int? result) => result!); @override Future getScrollY() => - _channel.invokeMethod("getScrollY").then((result) => result!); + _channel.invokeMethod('getScrollY').then((int? result) => result!); /// Method channel implementation for [WebViewPlatform.clearCookies]. static Future clearCookies() { return _cookieManagerChannel .invokeMethod('clearCookies') - .then((dynamic result) => result!); + .then((dynamic result) => result! as bool); + } + + /// Method channel implementation for [WebViewPlatform.setCookie]. + static Future setCookie(WebViewCookie cookie) { + return _cookieManagerChannel.invokeMethod( + 'setCookie', cookie.toJson()); } static Map _webSettingsToMap(WebSettings? settings) { @@ -200,6 +270,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { _addIfNonNull( 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); _addSettingIfPresent('userAgent', settings.userAgent); + _addIfNonNull('zoomEnabled', settings.zoomEnabled); return map; } @@ -218,6 +289,10 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { 'userAgent': creationParams.userAgent, 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, 'usesHybridComposition': usesHybridComposition, + 'backgroundColor': creationParams.backgroundColor?.value, + 'cookies': creationParams.cookies + .map((WebViewCookie cookie) => cookie.toJson()) + .toList() }; } } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart index 43f967fb13b0..a6967a5410f4 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. export 'javascript_channel_registry.dart'; +export 'webview_cookie_manager.dart'; export 'webview_platform.dart'; export 'webview_platform_callbacks_handler.dart'; export 'webview_platform_controller.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_cookie_manager.dart new file mode 100644 index 000000000000..1f87e6a26ba6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_cookie_manager.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 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/src/types/webview_cookie.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 +/// [WebViewCookieManagerPlatform] methods. +abstract class WebViewCookieManagerPlatform extends PlatformInterface { + /// Constructs a WebViewCookieManagerPlatform. + WebViewCookieManagerPlatform() : super(token: _token); + + static final Object _token = Object(); + + static WebViewCookieManagerPlatform? _instance; + + /// The instance of [WebViewCookieManagerPlatform] to use. + static WebViewCookieManagerPlatform? get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [WebViewCookieManagerPlatform] when they register themselves. + static set instance(WebViewCookieManagerPlatform? instance) { + if (instance == null) { + throw AssertionError( + 'Platform interfaces can only be set to a non-null instance'); + } + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// 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/src/platform_interface/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart index 4732f54d6456..e35635d73a9e 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart @@ -59,8 +59,9 @@ abstract class WebViewPlatform { /// Clears all cookies for all [WebView] instances. /// /// Returns true if cookies were present before clearing, else false. + /// Soon to be deprecated. 'Use `WebViewCookieManagerPlatform.clearCookies` instead. Future clearCookies() { throw UnimplementedError( - "WebView clearCookies is not implemented on the current platform"); + 'WebView clearCookies is not implemented on the current platform'); } } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart index 319ca7e7a845..3437fe1f2c09 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart @@ -21,8 +21,48 @@ abstract class WebViewPlatformController { /// Callbacks made by the WebView will be delegated to `handler`. /// /// The `handler` parameter must not be null. + // TODO(mvanbeusekom): Remove unused constructor parameter with the next + // breaking change (see issue https://github.com/flutter/flutter/issues/94292). + // ignore: avoid_unused_constructor_parameters WebViewPlatformController(WebViewPlatformCallbacksHandler handler); + /// 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( + 'WebView 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( + 'WebView 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( + 'WebView loadHtmlString is not implemented on the current platform'); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will @@ -36,7 +76,26 @@ abstract class WebViewPlatformController { Map? headers, ) { throw UnimplementedError( - "WebView loadUrl is not implemented on the current platform"); + 'WebView loadUrl 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( + WebViewRequest request, + ) { + throw UnimplementedError( + 'WebView loadRequest is not implemented on the current platform'); } /// Updates the webview settings. @@ -45,7 +104,7 @@ abstract class WebViewPlatformController { /// All null fields in `settings` are ignored. Future updateSettings(WebSettings setting) { throw UnimplementedError( - "WebView updateSettings is not implemented on the current platform"); + 'WebView updateSettings is not implemented on the current platform'); } /// Accessor to the current URL that the WebView is displaying. @@ -53,19 +112,19 @@ abstract class WebViewPlatformController { /// If no URL was ever loaded, returns `null`. Future currentUrl() { throw UnimplementedError( - "WebView currentUrl is not implemented on the current platform"); + 'WebView currentUrl is not implemented on the current platform'); } /// Checks whether there's a back history item. Future canGoBack() { throw UnimplementedError( - "WebView canGoBack is not implemented on the current platform"); + 'WebView canGoBack is not implemented on the current platform'); } /// Checks whether there's a forward history item. Future canGoForward() { throw UnimplementedError( - "WebView canGoForward is not implemented on the current platform"); + 'WebView canGoForward is not implemented on the current platform'); } /// Goes back in the history of this WebView. @@ -73,7 +132,7 @@ abstract class WebViewPlatformController { /// If there is no back history item this is a no-op. Future goBack() { throw UnimplementedError( - "WebView goBack is not implemented on the current platform"); + 'WebView goBack is not implemented on the current platform'); } /// Goes forward in the history of this WebView. @@ -81,13 +140,13 @@ abstract class WebViewPlatformController { /// If there is no forward history item this is a no-op. Future goForward() { throw UnimplementedError( - "WebView goForward is not implemented on the current platform"); + 'WebView goForward is not implemented on the current platform'); } /// Reloads the current URL. Future reload() { throw UnimplementedError( - "WebView reload is not implemented on the current platform"); + 'WebView reload is not implemented on the current platform'); } /// Clears all caches used by the [WebView]. @@ -100,16 +159,34 @@ abstract class WebViewPlatformController { /// 4. Local Storage. Future clearCache() { throw UnimplementedError( - "WebView clearCache is not implemented on the current platform"); + 'WebView clearCache is not implemented on the current platform'); } /// Evaluates a JavaScript expression in the context of the current page. /// /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { + /// evaluated expression is not supported (e.g on iOS not all non-primitive types can be evaluated). + Future evaluateJavascript(String javascript) { + throw UnimplementedError( + 'WebView evaluateJavascript 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( + 'WebView 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( - "WebView evaluateJavascript is not implemented on the current platform"); + 'WebView runJavascriptReturningResult is not implemented on the current platform'); } /// Adds new JavaScript channels to the set of enabled channels. @@ -125,22 +202,22 @@ abstract class WebViewPlatformController { /// See also: [CreationParams.javascriptChannelNames]. Future addJavascriptChannels(Set javascriptChannelNames) { throw UnimplementedError( - "WebView addJavascriptChannels is not implemented on the current platform"); + 'WebView addJavascriptChannels is not implemented on the current platform'); } /// Removes JavaScript channel names from the set of enabled channels. /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through + /// This disables channels that were previously enabled by [addJavascriptChannels] or through /// [CreationParams.javascriptChannelNames]. Future removeJavascriptChannels(Set javascriptChannelNames) { throw UnimplementedError( - "WebView removeJavascriptChannels is not implemented on the current platform"); + 'WebView removeJavascriptChannels is not implemented on the current platform'); } /// Returns the title of the currently loaded page. Future getTitle() { throw UnimplementedError( - "WebView getTitle is not implemented on the current platform"); + 'WebView getTitle is not implemented on the current platform'); } /// Set the scrolled position of this view. @@ -148,7 +225,7 @@ abstract class WebViewPlatformController { /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. Future scrollTo(int x, int y) { throw UnimplementedError( - "WebView scrollTo is not implemented on the current platform"); + 'WebView scrollTo is not implemented on the current platform'); } /// Move the scrolled position of this view. @@ -156,7 +233,7 @@ abstract class WebViewPlatformController { /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. Future scrollBy(int x, int y) { throw UnimplementedError( - "WebView scrollBy is not implemented on the current platform"); + 'WebView scrollBy is not implemented on the current platform'); } /// Return the horizontal scroll position of this view. @@ -164,7 +241,7 @@ abstract class WebViewPlatformController { /// Scroll position is measured from left. Future getScrollX() { throw UnimplementedError( - "WebView getScrollX is not implemented on the current platform"); + 'WebView getScrollX is not implemented on the current platform'); } /// Return the vertical scroll position of this view. @@ -172,6 +249,6 @@ abstract class WebViewPlatformController { /// Scroll position is measured from top. Future getScrollY() { throw UnimplementedError( - "WebView getScrollY is not implemented on the current platform"); + 'WebView getScrollY is not implemented on the current platform'); } } 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 f213e976ad84..c1763cdae501 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 @@ -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 'auto_media_playback_policy.dart'; -import 'web_settings.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; /// Configuration to use when creating a new [WebViewPlatformController]. /// @@ -20,6 +20,8 @@ class CreationParams { this.userAgent, this.autoMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.backgroundColor, + this.cookies = const [], }) : assert(autoMediaPlaybackPolicy != null); /// The initialUrl to load in the webview. @@ -53,8 +55,16 @@ class CreationParams { /// Which restrictions apply on automatic media playback. final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; + /// The background color of the webview. + /// + /// When null the platform's webview default background color is used. + final Color? backgroundColor; + + /// The initial set of cookies to set before the webview does its first load. + final List cookies; + @override String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; + return 'CreationParams(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent, backgroundColor: $backgroundColor, cookies: $cookies)'; } } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart index 8b31f5b6061e..e68cc2ef1291 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart @@ -4,14 +4,14 @@ import 'javascript_message.dart'; -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); +/// Callback type for handling messages sent from JavaScript running in a web view. +typedef JavascriptMessageHandler = void Function(JavascriptMessage message); -final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); +final RegExp _validChannelNames = RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$'); /// A named channel for receiving messaged from JavaScript code running inside a web view. class JavascriptChannel { - /// Constructs a Javascript channel. + /// Constructs a JavaScript channel. /// /// The parameters `name` and `onMessageReceived` must not be null. JavascriptChannel({ @@ -24,7 +24,7 @@ class JavascriptChannel { /// The channel's name. /// /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. + /// the JavaScript window object's property named `name`. /// /// The name must start with a letter or underscore(_), followed by any combination of those /// characters plus digits. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart index b1a9b9b9daa8..f2bcf19f42fd 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -10,3 +10,5 @@ export 'javascript_mode.dart'; export 'web_resource_error.dart'; export 'web_resource_error_type.dart'; export 'web_settings.dart'; +export 'webview_cookie.dart'; +export 'webview_request.dart'; 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 48b2de9c1ca0..102ab10ccea7 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 @@ -7,20 +7,21 @@ import 'package:flutter/widgets.dart'; import 'javascript_mode.dart'; /// A single setting for configuring a WebViewPlatform which may be absent. +@immutable class WebSetting { /// Constructs an absent setting instance. /// /// The [isPresent] field for the instance will be false. /// /// Accessing [value] for an absent instance will throw. - WebSetting.absent() + const WebSetting.absent() : _value = null, isPresent = false; /// Constructs a setting of the given `value`. /// /// The [isPresent] field for the instance will be true. - WebSetting.of(T value) + const WebSetting.of(T value) : _value = value, isPresent = true; @@ -51,13 +52,17 @@ class WebSetting { @override bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - final WebSetting typedOther = other as WebSetting; - return typedOther.isPresent == isPresent && typedOther._value == _value; + if (other.runtimeType != runtimeType) { + return false; + } + + return other is WebSetting && + other.isPresent == isPresent && + other._value == _value; } @override - int get hashCode => hashValues(_value, isPresent); + int get hashCode => Object.hash(_value, isPresent); } /// Settings for configuring a WebViewPlatform. @@ -78,6 +83,7 @@ class WebSettings { this.debuggingEnabled, this.gestureNavigationEnabled, this.allowsInlineMediaPlayback, + this.zoomEnabled, required this.userAgent, }) : assert(userAgent != null); @@ -111,6 +117,9 @@ class WebSettings { /// See also [WebView.userAgent]. final WebSetting userAgent; + /// Sets whether the WebView should support zooming using its on-screen zoom controls and gestures. + final bool? zoomEnabled; + /// Whether to allow swipe based navigation in iOS. /// /// See also: [WebView.gestureNavigationEnabled] diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.dart new file mode 100644 index 000000000000..406c510afd4b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.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. + +/// A cookie that can be set globally for all web views +/// using [WebViewCookieManagerPlatform]. +class WebViewCookie { + /// Constructs a new [WebViewCookie]. + 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. + /// Is set to `/` in the constructor 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; + + /// Serializes the [WebViewCookie] to a Map. + Map toJson() { + return { + 'name': name, + 'value': value, + 'domain': domain, + 'path': path + }; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart new file mode 100644 index 000000000000..940e3a25f4ba --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.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 'dart:typed_data'; + +/// Defines the supported HTTP methods for loading a page in [WebView]. +enum WebViewRequestMethod { + /// HTTP GET method. + get, + + /// HTTP POST method. + post, +} + +/// Extension methods on the [WebViewRequestMethod] enum. +extension WebViewRequestMethodExtensions on WebViewRequestMethod { + /// Converts [WebViewRequestMethod] to [String] format. + String serialize() { + switch (this) { + case WebViewRequestMethod.get: + return 'get'; + case WebViewRequestMethod.post: + return 'post'; + } + } +} + +/// Defines the parameters that can be used to load a page in the [WebView]. +class WebViewRequest { + /// Creates the [WebViewRequest]. + WebViewRequest({ + required this.uri, + required this.method, + this.headers = const {}, + this.body, + }); + + /// URI for the request. + final Uri uri; + + /// HTTP method used to make the request. + final WebViewRequestMethod method; + + /// Headers for the request. + final Map headers; + + /// HTTP body for the request. + final Uint8List? body; + + /// Serializes the [WebViewRequest] to JSON. + Map toJson() => { + 'uri': uri.toString(), + 'method': method.serialize(), + 'headers': headers, + 'body': body, + }; +} 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..1812d7e39c29 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_widget_creation_params.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/foundation.dart'; +import 'package:flutter/gestures.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 WKWebViewWidgetCreationParams extends PlatformWebViewWidgetCreationParams { +/// WKWebViewWidgetCreationParams._( +/// // This parameter prevents breaking changes later. +/// // ignore: avoid_unused_constructor_parameters +/// PlatformWebViewWidgetCreationParams params, { +/// this.domain, +/// }) : super( +/// key: params.key, +/// controller: params.controller, +/// gestureRecognizers: params.gestureRecognizers, +/// ); +/// +/// factory WKWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( +/// PlatformWebViewWidgetCreationParams params, { +/// String? domain, +/// }) { +/// return WKWebViewWidgetCreationParams._(params, domain: domain); +/// } +/// +/// final String? domain; +/// } +/// ``` +/// {@end-tool} +@immutable +class PlatformWebViewWidgetCreationParams { + /// Used by the platform implementation to create a new [PlatformWebViewWidget]. + const PlatformWebViewWidgetCreationParams({ + this.key, + required this.controller, + 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 `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/lib/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart index b508989ed978..aa41c8285975 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart @@ -2,6 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/method_channel/webview_method_channel.dart'; export 'src/platform_interface/platform_interface.dart'; export 'src/types/types.dart'; -export 'src/method_channel/webview_method_channel.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index bf43c265d77a..c14df52371a8 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -1,22 +1,24 @@ name: webview_flutter_platform_interface description: A common platform interface for the webview_flutter plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_platform_interface +repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_platform_interface issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%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.9.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.0 + 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 \ No newline at end of file + 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 2f845eaa4999..8b9a4ceebc91 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,11 +2,16 @@ // 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'; import 'package:mockito/mockito.dart'; - -import 'package:webview_flutter_platform_interface/src/method_channel/webview_method_channel.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; void main() { @@ -31,6 +36,36 @@ void main() { case 'canGoBack': case 'canGoForward': return true; + case 'loadFile': + if (methodCall.arguments == 'invalid file') { + throw PlatformException( + code: 'loadFile_failed', + message: 'Failed loading file.', + details: null); + } else if (methodCall.arguments == 'some error') { + throw PlatformException( + code: 'some_error', + message: 'Some error occurred.', + details: null, + ); + } + return null; + case 'loadFlutterAsset': + if (methodCall.arguments == 'invalid key') { + 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; + case 'runJavascriptReturningResult': case 'evaluateJavascript': return methodCall.arguments as String; case 'getScrollX': @@ -55,6 +90,133 @@ void main() { log.clear(); }); + test('loadFile', () async { + await webViewPlatform.loadFile( + '/folder/asset.html', + ); + + expect( + log, + [ + isMethodCall( + 'loadFile', + arguments: '/folder/asset.html', + ), + ], + ); + }); + + test('loadFile with invalid file', () async { + expect( + () => webViewPlatform.loadFile('invalid file'), + throwsA( + isA().having( + (ArgumentError error) => error.message, + 'message', + 'Failed loading file.', + ), + ), + ); + }); + + test('loadFile with some error.', () async { + expect( + () => webViewPlatform.loadFile('some error'), + throwsA( + isA().having( + (PlatformException error) => error.message, + 'message', + 'Some error occurred.', + ), + ), + ); + }); + + test('loadFlutterAsset', () async { + await webViewPlatform.loadFlutterAsset( + 'folder/asset.html', + ); + + expect( + log, + [ + isMethodCall( + 'loadFlutterAsset', + arguments: 'folder/asset.html', + ), + ], + ); + }); + + test('loadFlutterAsset with empty key', () async { + expect(() => webViewPlatform.loadFlutterAsset(''), throwsAssertionError); + }); + + test('loadFlutterAsset with invalid key', () async { + expect( + () => webViewPlatform.loadFlutterAsset('invalid key'), + throwsA( + isA().having( + (ArgumentError error) => error.message, + 'message', + 'Failed loading asset.', + ), + ), + ); + }); + + test('loadFlutterAsset with some error.', () async { + expect( + () => webViewPlatform.loadFlutterAsset('some error'), + throwsA( + isA().having( + (PlatformException error) => error.message, + 'message', + 'Some error occurred.', + ), + ), + ); + }); + + test('loadHtmlString without base URL', () async { + await webViewPlatform.loadHtmlString( + 'Test HTML string', + ); + + expect( + log, + [ + isMethodCall( + 'loadHtmlString', + arguments: { + 'html': 'Test HTML string', + 'baseUrl': null, + }, + ), + ], + ); + }); + + test('loadHtmlString without base URL', () async { + await webViewPlatform.loadHtmlString( + 'Test HTML string', + baseUrl: 'https://flutter.dev', + ); + + expect( + log, + [ + isMethodCall( + 'loadHtmlString', + arguments: { + 'html': 'Test HTML string', + 'baseUrl': 'https://flutter.dev', + }, + ), + ], + ); + }); + test('loadUrl with headers', () async { await webViewPlatform.loadUrl( 'https://test.url', @@ -101,6 +263,56 @@ void main() { ); }); + test('loadRequest', () async { + await webViewPlatform.loadRequest(WebViewRequest( + uri: Uri.parse('https://test.url'), + method: WebViewRequestMethod.get, + )); + + expect( + log, + [ + isMethodCall( + 'loadRequest', + arguments: { + 'request': { + 'uri': 'https://test.url', + 'method': 'get', + 'headers': {}, + 'body': null, + } + }, + ), + ], + ); + }); + + test('loadRequest with optional parameters', () async { + await webViewPlatform.loadRequest(WebViewRequest( + uri: Uri.parse('https://test.url'), + method: WebViewRequestMethod.get, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('hello world'.codeUnits), + )); + + expect( + log, + [ + isMethodCall( + 'loadRequest', + arguments: { + 'request': { + 'uri': 'https://test.url', + 'method': 'get', + 'headers': {'foo': 'bar'}, + 'body': 'hello world'.codeUnits, + } + }, + ), + ], + ); + }); + test('currentUrl', () async { final String? currentUrl = await webViewPlatform.currentUrl(); @@ -204,7 +416,7 @@ void main() { test('updateSettings', () async { final WebSettings settings = - WebSettings(userAgent: WebSetting.of('Dart Test')); + WebSettings(userAgent: const WebSetting.of('Dart Test')); await webViewPlatform.updateSettings(settings); expect( @@ -222,13 +434,14 @@ void main() { test('updateSettings all parameters', () async { final WebSettings settings = WebSettings( - userAgent: WebSetting.of('Dart Test'), + userAgent: const WebSetting.of('Dart Test'), javascriptMode: JavascriptMode.disabled, hasNavigationDelegate: true, hasProgressTracking: true, debuggingEnabled: true, gestureNavigationEnabled: true, allowsInlineMediaPlayback: true, + zoomEnabled: false, ); await webViewPlatform.updateSettings(settings); @@ -245,6 +458,7 @@ void main() { 'debuggingEnabled': true, 'gestureNavigationEnabled': true, 'allowsInlineMediaPlayback': true, + 'zoomEnabled': false, }, ), ], @@ -253,7 +467,7 @@ void main() { test('updateSettings without settings', () async { final WebSettings settings = - WebSettings(userAgent: WebSetting.absent()); + WebSettings(userAgent: const WebSetting.absent()); await webViewPlatform.updateSettings(settings); expect( @@ -265,16 +479,50 @@ void main() { test('evaluateJavascript', () async { final String evaluateJavascript = await webViewPlatform.evaluateJavascript( - 'This simulates some Javascript code.', + 'This simulates some JavaScript code.', ); - expect('This simulates some Javascript code.', evaluateJavascript); + expect('This simulates some JavaScript code.', evaluateJavascript); expect( log, [ isMethodCall( 'evaluateJavascript', - arguments: 'This simulates some Javascript code.', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('runJavascript', () async { + await webViewPlatform.runJavascript( + 'This simulates some JavaScript code.', + ); + + expect( + log, + [ + isMethodCall( + 'runJavascript', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('runJavascriptReturningResult', () async { + final String evaluateJavascript = + await webViewPlatform.runJavascriptReturningResult( + 'This simulates some JavaScript code.', + ); + + expect('This simulates some JavaScript code.', evaluateJavascript); + expect( + log, + [ + isMethodCall( + 'runJavascriptReturningResult', + arguments: 'This simulates some JavaScript code.', ), ], ); @@ -409,6 +657,32 @@ void main() { ], ); }); + + test('backgroundColor is null by default', () { + final CreationParams creationParams = CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.of('Dart Test'), + ), + ); + final Map creationParamsMap = + MethodChannelWebViewPlatform.creationParamsToMap(creationParams); + + expect(creationParamsMap['backgroundColor'], null); + }); + + test('backgroundColor is converted to an int', () { + const Color whiteColor = Color(0xFFFFFFFF); + final CreationParams creationParams = CreationParams( + backgroundColor: whiteColor, + webSettings: WebSettings( + userAgent: const WebSetting.of('Dart Test'), + ), + ); + final Map creationParamsMap = + MethodChannelWebViewPlatform.creationParamsToMap(creationParams); + + expect(creationParamsMap['backgroundColor'], whiteColor.value); + }); }); group('Tests on `plugins.flutter.io/cookie_manager` channel', () { @@ -447,6 +721,26 @@ void main() { ], ); }); + + test('setCookie', () async { + await MethodChannelWebViewPlatform.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_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 55d0e1e13bd1..30795b01c83f 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 @@ -3,9 +3,8 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; -import 'package:webview_flutter_platform_interface/src/types/types.dart'; import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; void main() { final Map _log = {}; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/webview_cookie_manager_test.dart new file mode 100644 index 000000000000..e0aae2146abc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/webview_cookie_manager_test.dart @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/platform_interface/platform_interface.dart'; +import 'package:webview_flutter_platform_interface/src/types/webview_cookie.dart'; + +void main() { + WebViewCookieManagerPlatform? cookieManager; + + setUp(() { + cookieManager = TestWebViewCookieManagerPlatform(); + }); + + test('clearCookies should throw UnimplementedError', () { + expect(() => cookieManager!.clearCookies(), throwsUnimplementedError); + }); + + test('setCookie should throw UnimplementedError', () { + const WebViewCookie cookie = + WebViewCookie(domain: 'flutter.dev', name: 'foo', value: 'bar'); + expect(() => cookieManager!.setCookie(cookie), throwsUnimplementedError); + }); +} + +class TestWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_cookie_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_cookie_test.dart new file mode 100644 index 000000000000..f058b8649b96 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_cookie_test.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_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; + +void main() { + test('WebViewCookie should serialize correctly', () { + WebViewCookie cookie; + Map serializedCookie; + // Test serialization + cookie = const WebViewCookie( + name: 'foo', value: 'bar', domain: 'example.com', path: '/test'); + serializedCookie = cookie.toJson(); + expect(serializedCookie['name'], 'foo'); + expect(serializedCookie['value'], 'bar'); + expect(serializedCookie['domain'], 'example.com'); + expect(serializedCookie['path'], '/test'); + }); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart new file mode 100644 index 000000000000..6e1a4d7b4d56 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_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 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; + +void main() { + test('WebViewRequestMethod should serialize correctly', () { + expect(WebViewRequestMethod.get.serialize(), 'get'); + expect(WebViewRequestMethod.post.serialize(), 'post'); + }); + + test('WebViewRequest should serialize correctly', () { + WebViewRequest request; + Map serializedRequest; + // Test serialization without headers or a body + request = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.get, + ); + serializedRequest = request.toJson(); + expect(serializedRequest['uri'], 'https://flutter.dev'); + expect(serializedRequest['method'], 'get'); + expect(serializedRequest['headers'], {}); + expect(serializedRequest['body'], null); + // Test serialization of headers and body + request = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.get, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('Example Body'.codeUnits), + ); + serializedRequest = request.toJson(); + expect(serializedRequest['headers'], {'foo': 'bar'}); + expect(serializedRequest['body'], 'Example Body'.codeUnits); + }); +} 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..5674c1522408 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_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: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); + }, throwsNoSuchMethodError); + }); + + 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..b6d043cac9c8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart @@ -0,0 +1,467 @@ +// Copyright 2013 The Flutter Authors. 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()); + }, throwsNoSuchMethodError); + }); + + 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..30fa52ece24a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.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/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); + }, throwsNoSuchMethodError); + }); + + 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..f09156919512 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart @@ -0,0 +1,109 @@ +// Copyright 2013 The Flutter Authors. 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(); + }, throwsNoSuchMethodError); + }); + + 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/AUTHORS b/packages/webview_flutter/webview_flutter_web/AUTHORS new file mode 100644 index 000000000000..05432a7fbf9a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/AUTHORS @@ -0,0 +1,8 @@ +# 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. +Bodhi Mulders + diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md new file mode 100644 index 000000000000..631608689a7a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -0,0 +1,22 @@ +## 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 + +* Adds an explanation of registering the implementation in the README. + +## 0.1.0 + +* First web implementation for webview_flutter diff --git a/packages/webview_flutter/webview_flutter_web/LICENSE b/packages/webview_flutter/webview_flutter_web/LICENSE new file mode 100644 index 000000000000..77130909e474 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/LICENSE @@ -0,0 +1,26 @@ +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/webview_flutter/webview_flutter_web/README.md b/packages/webview_flutter/webview_flutter_web/README.md new file mode 100644 index 000000000000..a7711ee171e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/README.md @@ -0,0 +1,78 @@ +# webview\_flutter\_web + +This is an implementation of the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin for web. + +It is currently severely limited and doesn't implement most of the available functionality. +The following functionality is currently available: + +- `loadUrl` (Without headers) +- `requestUrl` +- `loadHTMLString` (Without `baseUrl`) +- Setting the `initialUrl` through `CreationParams`. + +Nothing else is currently supported. + +## Usage + +This package is not an endorsed implementation of the `webview_flutter` plugin +yet, so it currently requires extra setup to use: + +* [Add this package](https://pub.dev/packages/webview_flutter_web/install) + as an explicit dependency of your project, in addition to depending on + `webview_flutter`. +* Register `WebWebViewPlatform` as the `WebView.platform` before creating a + `WebView`. See below for examples. + +Once those steps below are complete, the APIs from `webview_flutter` listed +above can be used as normal on web. + +### Registering the implementation + +Before creating a `WebView` (for instance, at the start of `main`), you will +need to register the web implementation. + +#### Web-only project example + +```dart +... +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; + +main() { + WebView.platform = WebWebViewPlatform(); + ... +``` + +#### Multi-platform project example + +If your project supports platforms other than web, you will need to use a +conditional import to avoid directly including `webview_flutter_web.dart` on +non-web platforms. For example: + +`register_web_webview.dart`: +```dart +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; + +void registerWebViewWebImplementation() { + WebView.platform = WebWebViewPlatform(); +} +``` + +`register_web_webview_stub.dart`: +```dart +void registerWebViewWebImplementation() { + // No-op. +} +``` + +`main.dart`: +```dart +... +import 'register_web_webview_stub.dart' + if (dart.library.html) 'register_web.dart'; + +main() { + registerWebViewWebImplementation(); + ... +``` diff --git a/packages/webview_flutter/webview_flutter_web/example/.metadata b/packages/webview_flutter/webview_flutter_web/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/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: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_web/example/README.md b/packages/webview_flutter/webview_flutter_web/example/README.md new file mode 100644 index 000000000000..e5bd6e20db63 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/README.md @@ -0,0 +1,3 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. diff --git a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..232ecdd302b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.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 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_web_example/web_view.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + await controllerCompleter.future; + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + expect(element!.src, primaryUrl); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + expect(element!.src, secondaryUrl); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart new file mode 100644 index 000000000000..c183625be634 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/lib/main.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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'web_view.dart'; + +void main() { + runApp(const MaterialApp(home: _WebViewExample())); +} + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + actions: [ + _SampleMenu(_controller.future), + ], + ), + body: WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + ), + ); + } +} + +enum _MenuOptions { + doPostRequest, +} + +class _SampleMenu extends StatelessWidget { + const _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + ], + ); + }, + ); + } + + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } +} 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 new file mode 100644 index 000000000000..ffd3367d33f4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart @@ -0,0 +1,384 @@ +// Copyright 2013 The Flutter Authors. 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:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.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); + +/// A web view widget for showing html content. +/// +/// The [WebView] widget wraps around the [WebWebViewPlatform]. +/// +/// The [WebView] widget is controlled using the [WebViewController] which is +/// provided through the `onWebViewCreated` callback. +/// +/// In this example project it's main purpose is to facilitate integration +/// testing of the `webview_flutter_web` package. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + }) : super(key: key); + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [WebWebViewPlatform]. + /// This property can be set to use a custom platform implementation for WebViews. + /// Setting `platform` doesn't affect [WebView]s that were already created. + static WebViewPlatform platform = WebWebViewPlatform(); + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// The initial URL to load. + final String? initialUrl; + + @override + State createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller.updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + final WebViewController controller = WebViewController( + widget, + webViewPlatformController!, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + ), + javascriptChannelRegistry: + JavascriptChannelRegistry({}), + ); + } +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(); + + @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) {} +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + /// Creates a [WebViewController] which can be used to control the provided + /// [WebView] widget. + WebViewController( + this._widget, + this._webViewPlatformController, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.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. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + /// Update the widget managed by the [WebViewController]. + Future updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When running JavaScript in a [WebView], it is best practice to wait for + // the [WebView.onPageFinished] callback. This guarantees all the JavaScript + // embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. + /// + /// Returns the evaluation result as a JSON formatted string. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript + /// embedded in the main frame HTML has been loaded. + Future runJavascriptReturningResult(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); + } + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + assert(newValue.zoomEnabled != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = const WebSetting.absent(); + bool? zoomEnabled; + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + if (currentValue.zoomEnabled != newValue.zoomEnabled) { + zoomEnabled = newValue.zoomEnabled; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + zoomEnabled: zoomEnabled, + ); + } + + // Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: false, + hasProgressTracking: false, + debuggingEnabled: false, + gestureNavigationEnabled: false, + allowsInlineMediaPlayback: true, + userAgent: const WebSetting.of(''), + zoomEnabled: false, + ); +} diff --git a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml new file mode 100644 index 000000000000..a98df799e92c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml @@ -0,0 +1,32 @@ +name: webview_flutter_web_example +description: Demonstrates how to use the webview_flutter_web plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + webview_flutter_web: + # When depending on this package from a real application you should use: + # webview_flutter_web: ^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: + 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/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/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/connectivity/connectivity/example/web/favicon.png b/packages/webview_flutter/webview_flutter_web/example/web/favicon.png similarity index 100% rename from packages/connectivity/connectivity/example/web/favicon.png rename to packages/webview_flutter/webview_flutter_web/example/web/favicon.png diff --git a/packages/connectivity/connectivity/example/web/icons/Icon-192.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-192.png similarity index 100% rename from packages/connectivity/connectivity/example/web/icons/Icon-192.png rename to packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-192.png diff --git a/packages/connectivity/connectivity/example/web/icons/Icon-512.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-512.png similarity index 100% rename from packages/connectivity/connectivity/example/web/icons/Icon-512.png rename to packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-512.png diff --git a/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-192.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000000..eb9b4d76e525 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-192.png differ diff --git a/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-512.png b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000000..d69c56691fbd Binary files /dev/null and b/packages/webview_flutter/webview_flutter_web/example/web/icons/Icon-maskable-512.png differ diff --git a/packages/webview_flutter/webview_flutter_web/example/web/index.html b/packages/webview_flutter/webview_flutter_web/example/web/index.html new file mode 100644 index 000000000000..8b8b5bf92f89 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + webview_flutter_web Example + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_web/example/web/manifest.json b/packages/webview_flutter/webview_flutter_web/example/web/manifest.json new file mode 100644 index 000000000000..1124a93355ec --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "webview_flutter_web Example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart new file mode 100644 index 000000000000..1724dd60eab4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.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. + +/// This file shims dart:ui in web-only scenarios, getting rid of the need to +/// suppress analyzer warnings. + +// TODO(BeMacized): Remove this file once web-only dart:ui APIs, +// are exposed from a dedicated place. flutter/flutter#55000 +export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart'; 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 new file mode 100644 index 000000000000..40d8f1903111 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.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 '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/main/lib/web_ui/lib/ui.dart#L62 +class platformViewRegistry { + /// Shim for registerViewFactory + /// 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/main/lib/web_ui/lib/src/engine/assets.dart#L12 +class webOnlyAssetManager { + /// Shim for getAssetUrl. + /// 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. +typedef VoidCallback = void Function(); diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.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 'dart:ui'; 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 new file mode 100644 index 000000000000..adf6495b8f2a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart @@ -0,0 +1,291 @@ +// Copyright 2013 The Flutter Authors. 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:html'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'shims/dart_ui.dart' as ui; + +/// Builds an iframe based WebView. +/// +/// This is used as the default implementation for [WebView.platform] on web. +class WebWebViewPlatform implements WebViewPlatform { + /// Constructs a new instance of [WebWebViewPlatform]. + WebWebViewPlatform() { + ui.platformViewRegistry.registerViewFactory( + 'webview-iframe', + (int viewId) => IFrameElement() + ..id = 'webview-$viewId' + ..width = '100%' + ..height = '100%' + ..style.border = 'none'); + } + + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry? javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + return HtmlElementView( + viewType: 'webview-iframe', + onPlatformViewCreated: (int viewId) { + if (onWebViewPlatformCreated == null) { + return; + } + final IFrameElement element = + document.getElementById('webview-$viewId')! as IFrameElement; + if (creationParams.initialUrl != null) { + // ignore: unsafe_html + element.src = creationParams.initialUrl; + } + onWebViewPlatformCreated(WebWebViewPlatformController( + element, + )); + }, + ); + } + + @override + Future clearCookies() async => false; + + /// Gets called when the plugin is registered. + static void registerWith(Registrar registrar) {} +} + +/// Implementation of [WebViewPlatformController] for web. +class WebWebViewPlatformController implements WebViewPlatformController { + /// Constructs a [WebWebViewPlatformController]. + WebWebViewPlatformController(this._element); + + final IFrameElement _element; + HttpRequestFactory _httpRequestFactory = HttpRequestFactory(); + + /// Setter for setting the HttpRequestFactory, for testing purposes. + @visibleForTesting + // ignore: avoid_setters_without_getters + set httpRequestFactory(HttpRequestFactory factory) { + _httpRequestFactory = factory; + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError(); + } + + @override + Future canGoBack() { + throw UnimplementedError(); + } + + @override + Future canGoForward() { + throw UnimplementedError(); + } + + @override + Future clearCache() { + throw UnimplementedError(); + } + + @override + Future currentUrl() { + throw UnimplementedError(); + } + + @override + Future evaluateJavascript(String javascript) { + throw UnimplementedError(); + } + + @override + Future getScrollX() { + throw UnimplementedError(); + } + + @override + Future getScrollY() { + throw UnimplementedError(); + } + + @override + Future getTitle() { + throw UnimplementedError(); + } + + @override + Future goBack() { + throw UnimplementedError(); + } + + @override + Future goForward() { + throw UnimplementedError(); + } + + @override + Future loadUrl(String url, Map? headers) async { + // ignore: unsafe_html + _element.src = url; + } + + @override + Future reload() { + throw UnimplementedError(); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError(); + } + + @override + Future runJavascript(String javascript) { + throw UnimplementedError(); + } + + @override + Future runJavascriptReturningResult(String javascript) { + throw UnimplementedError(); + } + + @override + Future scrollBy(int x, int y) { + throw UnimplementedError(); + } + + @override + Future scrollTo(int x, int y) { + throw UnimplementedError(); + } + + @override + Future updateSettings(WebSettings setting) { + throw UnimplementedError(); + } + + @override + Future loadFile(String absoluteFilePath) { + throw UnimplementedError(); + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) async { + // ignore: unsafe_html + _element.src = Uri.dataFromString( + html, + mimeType: 'text/html', + encoding: utf8, + ).toString(); + } + + @override + Future loadRequest(WebViewRequest request) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + final HttpRequest httpReq = await _httpRequestFactory.request( + request.uri.toString(), + method: request.method.serialize(), + requestHeaders: request.headers, + sendData: request.body); + final String contentType = + httpReq.getResponseHeader('content-type') ?? 'text/html'; + // ignore: unsafe_html + _element.src = Uri.dataFromString( + httpReq.responseText ?? '', + mimeType: contentType, + encoding: utf8, + ).toString(); + } + + @override + Future loadFlutterAsset(String key) { + throw UnimplementedError(); + } +} + +/// Factory class for creating [HttpRequest] instances. +class HttpRequestFactory { + /// Creates and sends a URL request for the specified [url]. + /// + /// By default `request` will perform an HTTP GET request, but a different + /// method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the + /// [method] parameter. (See also [HttpRequest.postFormData] for `POST` + /// requests only. + /// + /// The Future is completed when the response is available. + /// + /// If specified, `sendData` will send data in the form of a [ByteBuffer], + /// [Blob], [Document], [String], or [FormData] along with the HttpRequest. + /// + /// If specified, [responseType] sets the desired response format for the + /// request. By default it is [String], but can also be 'arraybuffer', 'blob', + /// 'document', 'json', or 'text'. See also [HttpRequest.responseType] + /// for more information. + /// + /// The [withCredentials] parameter specified that credentials such as a cookie + /// (already) set in the header or + /// [authorization headers](http://tools.ietf.org/html/rfc1945#section-10.2) + /// should be specified for the request. Details to keep in mind when using + /// credentials: + /// + /// /// Using credentials is only useful for cross-origin requests. + /// /// The `Access-Control-Allow-Origin` header of `url` cannot contain a wildcard (///). + /// /// The `Access-Control-Allow-Credentials` header of `url` must be set to true. + /// /// If `Access-Control-Expose-Headers` has not been set to true, only a subset of all the response headers will be returned when calling [getAllResponseHeaders]. + /// + /// The following is equivalent to the [getString] sample above: + /// + /// var name = Uri.encodeQueryComponent('John'); + /// var id = Uri.encodeQueryComponent('42'); + /// HttpRequest.request('users.json?name=$name&id=$id') + /// .then((HttpRequest resp) { + /// // Do something with the response. + /// }); + /// + /// Here's an example of submitting an entire form with [FormData]. + /// + /// var myForm = querySelector('form#myForm'); + /// var data = new FormData(myForm); + /// HttpRequest.request('/submit', method: 'POST', sendData: data) + /// .then((HttpRequest resp) { + /// // Do something with the response. + /// }); + /// + /// Note that requests for file:// URIs are only supported by Chrome extensions + /// with appropriate permissions in their manifest. Requests to file:// URIs + /// will also never fail- the Future will always complete successfully, even + /// when the file cannot be found. + /// + /// See also: [authorization headers](http://en.wikipedia.org/wiki/Basic_access_authentication). + Future request(String url, + {String? method, + bool? withCredentials, + String? responseType, + String? mimeType, + Map? requestHeaders, + dynamic sendData, + void Function(ProgressEvent e)? onProgress}) { + return HttpRequest.request(url, + method: method, + withCredentials: withCredentials, + responseType: responseType, + mimeType: mimeType, + requestHeaders: requestHeaders, + sendData: sendData, + onProgress: onProgress); + } +} diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml new file mode 100644 index 000000000000..12bec7242519 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/pubspec.yaml @@ -0,0 +1,32 @@ +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+4 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + web: + pluginClass: WebWebViewPlatform + fileName: webview_flutter_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + webview_flutter_platform_interface: ^1.8.0 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + mockito: ^5.0.0 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 new file mode 100644 index 000000000000..08337e42e661 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.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 'dart:html'; +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_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; +import './webview_flutter_web_test.mocks.dart'; + +@GenerateMocks([ + IFrameElement, + BuildContext, + CreationParams, + WebViewPlatformCallbacksHandler, + HttpRequestFactory, + HttpRequest, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebWebViewPlatform', () { + test('build returns a HtmlElementView', () { + // Setup + final WebWebViewPlatform platform = WebWebViewPlatform(); + // Run + final Widget widget = platform.build( + context: MockBuildContext(), + creationParams: CreationParams(), + webViewPlatformCallbacksHandler: MockWebViewPlatformCallbacksHandler(), + javascriptChannelRegistry: null, + ); + // Verify + expect(widget, isA()); + }); + }); + + group('WebWebViewPlatformController', () { + test('loadUrl sets url on iframe src attribute', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadUrl('test url', null); + // Verify + verify(mockElement.src = 'test url'); + }); + + 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', () { + test('loadRequest throws ArgumentError on missing scheme', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run & Verify + expect( + () async => await controller.loadRequest( + WebViewRequest( + uri: Uri.parse('flutter.dev'), + method: WebViewRequestMethod.get, + ), + ), + throwsA(const TypeMatcher())); + }); + + test('loadRequest makes request and loads response into iframe', + () async { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/plain'); + when(mockHttpRequest.responseText).thenReturn('test data'); + 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(mockHttpRequestFactory.request( + 'https://flutter.dev', + method: 'post', + requestHeaders: {'Foo': 'Bar'}, + sendData: Uint8List.fromList('test body'.codeUnits), + )); + 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 new file mode 100644 index 000000000000..e35d1e93c59f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart @@ -0,0 +1,1287 @@ +// Copyright 2013 The Flutter 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 +// in webview_flutter_web/test/webview_flutter_web_test.dart. +// Do not manually edit this file. + +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/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/webview_flutter_platform_interface.dart' + as _i9; +import 'package:webview_flutter_web/webview_flutter_web.dart' as _i10; + +// 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 _FakeCssClassSet_0 extends _i1.Fake implements _i2.CssClassSet {} + +class _FakeRectangle_1 extends _i1.Fake + implements _i3.Rectangle {} + +class _FakeCssRect_2 extends _i1.Fake implements _i2.CssRect {} + +class _FakePoint_3 extends _i1.Fake implements _i3.Point {} + +class _FakeElementEvents_4 extends _i1.Fake implements _i2.ElementEvents {} + +class _FakeCssStyleDeclaration_5 extends _i1.Fake + implements _i2.CssStyleDeclaration {} + +class _FakeElementStream_6 extends _i1.Fake + implements _i2.ElementStream {} + +class _FakeElementList_7 extends _i1.Fake + implements _i2.ElementList {} + +class _FakeScrollState_8 extends _i1.Fake implements _i2.ScrollState {} + +class _FakeAnimation_9 extends _i1.Fake implements _i2.Animation {} + +class _FakeElement_10 extends _i1.Fake implements _i2.Element {} + +class _FakeShadowRoot_11 extends _i1.Fake implements _i2.ShadowRoot {} + +class _FakeDocumentFragment_12 extends _i1.Fake + implements _i2.DocumentFragment {} + +class _FakeNode_13 extends _i1.Fake implements _i2.Node {} + +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 { + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_16 extends _i1.Fake implements _i5.DiagnosticsNode { + @override + String toString( + {_i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeHttpRequest_17 extends _i1.Fake implements _i2.HttpRequest {} + +class _FakeHttpRequestUpload_18 extends _i1.Fake + implements _i2.HttpRequestUpload {} + +class _FakeEvents_19 extends _i1.Fake implements _i2.Events {} + +/// A class which mocks [IFrameElement]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockIFrameElement extends _i1.Mock implements _i2.IFrameElement { + MockIFrameElement() { + _i1.throwOnMissingStub(this); + } + + @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); + @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>); + @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) => + super.noSuchMethod( + 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); + @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>); + @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>); + @override + 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(#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) => + 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}) => + 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}) => + (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}) => + 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); + @override + _i2.ShadowRoot attachShadow(Map? shadowRootInitDict) => + (super.noSuchMethod( + Invocation.method(#attachShadow, [shadowRootInitDict]), + returnValue: _FakeShadowRoot_11()) 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); + @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(#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]) => + super.noSuchMethod( + 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); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i4.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Widget get widget => (super.noSuchMethod(Invocation.getter(#widget), + returnValue: _FakeWidget_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}) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, [ancestor], {#aspect: aspect}), + returnValue: _FakeInheritedWidget_15()) 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(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_16()) 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); + @override + List<_i5.DiagnosticsNode> describeMissingAncestor( + {Type? expectedAncestorType}) => + (super.noSuchMethod( + 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(); +} + +/// A class which mocks [CreationParams]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCreationParams extends _i1.Mock implements _i7.CreationParams { + MockCreationParams() { + _i1.throwOnMissingStub(this); + } + + @override + 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(); +} + +/// A class which mocks [WebViewPlatformCallbacksHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformCallbacksHandler extends _i1.Mock + implements _i9.WebViewPlatformCallbacksHandler { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.FutureOr onNavigationRequest({String? url, 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(); +} + +/// A class which mocks [HttpRequestFactory]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpRequestFactory extends _i1.Mock + implements _i10.HttpRequestFactory { + MockHttpRequestFactory() { + _i1.throwOnMissingStub(this); + } + + @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}) => + (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(); +} + +/// A class which mocks [HttpRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { + MockHttpRequest() { + _i1.throwOnMissingStub(this); + } + + @override + Map get responseHeaders => + (super.noSuchMethod(Invocation.getter(#responseHeaders), + returnValue: {}) 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}) => + 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); + @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(#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); + @override + String toString() => super.toString(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS index 78f9e5ad9f6b..4fa8b35fca8a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS +++ b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS @@ -65,3 +65,5 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Maurits van Beusekom +Antonino Di Natale +Nick Bradshaw diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 1a85bc8a53e5..913b0a44b7ea 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,74 @@ +## 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 + +* Fixes header import for cookie manager to be relative only. + +## 2.7.0 + +* Adds implementation of the `loadFlutterAsset` method from the platform interface. + +## 2.6.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + +## 2.5.0 + +* Adds an option to set the background color of the webview. +* Migrates from `analysis_options_legacy.yaml` to `analysis_options.yaml`. +* Integration test fixes. +* Updates to webview_flutter_platform_interface version 1.5.2. + +## 2.4.0 + +* Implemented new `loadFile` and `loadHtmlString` methods from the platform interface. + +## 2.3.0 + +* Implemented new `loadRequest` method from platform interface. + +## 2.2.0 + +* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. + +## 2.1.0 + +* Add `zoomEnabled` functionality. + +## 2.0.14 + +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). + ## 2.0.13 * Extract WKWebView implementation from `webview_flutter`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md index 850ee74397a9..e5bd6e20db63 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/README.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/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_wkwebview/example/assets/www/index.html b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html new file mode 100644 index 000000000000..9895dd3ce6cb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/index.html @@ -0,0 +1,20 @@ + + + + +Load file or HTML string example + + + + +

Local demo page

+

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

+ + + \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css new file mode 100644 index 000000000000..c2140b8b0fd8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/www/styles/style.css @@ -0,0 +1,3 @@ +h1 { + color: blue; +} \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 33d5b340fa5a..1119f7457bc9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -2,28 +2,52 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + 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_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; +import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; import 'package:webview_flutter_wkwebview_example/navigation_decision.dart'; import 'package:webview_flutter_wkwebview_example/navigation_request.dart'; import 'package:webview_flutter_wkwebview_example/web_view.dart'; -void main() { +Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - // Set to `false` to include all flaky tests in the test run. See also https://github.com/flutter/flutter/issues/86757. - const bool _skipDueToIssue86757 = false; + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + 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(); @@ -32,7 +56,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -41,10 +65,30 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); - }, skip: _skipDueToIssue86757); + expect(currentUrl, primaryUrl); + }); + + testWidgets( + 'withWeakRefenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final Completer gcCompleter = Completer(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: gcCompleter.complete, + ); + + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + instance = null; + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + expect(gcCompleter.future, completion(0)); + }, timeout: const Timeout(Duration(seconds: 10))); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -53,7 +97,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -61,10 +105,31 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); + await controller.loadUrl(secondaryUrl); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); - }, skip: _skipDueToIssue86757); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = @@ -76,7 +141,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -94,20 +159,19 @@ void main() { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', - headers: headers); + await controller.loadUrl(headersUrl, headers: headers); final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + expect(currentUrl, headersUrl); await pageStarts.stream.firstWhere((String url) => url == currentUrl); await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }); - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -147,100 +211,29 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -292,7 +285,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( key: _globalKey, - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -397,7 +390,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -426,7 +420,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -457,7 +451,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -485,16 +480,16 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); - Completer pageLoaded = Completer(); - Completer videoPlaying = Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); await tester.pumpWidget( Directionality( @@ -511,7 +506,7 @@ void main() { onMessageReceived: (JavascriptMessage message) { final double currentTime = double.parse(message.message); // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1) { + if (currentTime > 1 && !videoPlaying.isCompleted) { videoPlaying.complete(null); } }, @@ -525,7 +520,7 @@ void main() { ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -534,18 +529,18 @@ void main() { // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - Completer controllerCompleter = + final Completer controllerCompleter = Completer(); - Completer pageLoaded = Completer(); - Completer videoPlaying = Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); await tester.pumpWidget( Directionality( @@ -562,7 +557,7 @@ void main() { onMessageReceived: (JavascriptMessage message) { final double currentTime = double.parse(message.message); // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1) { + if (currentTime > 1 && !videoPlaying.isCompleted) { videoPlaying.complete(null); } }, @@ -576,7 +571,7 @@ void main() { ), ), ); - WebViewController controller = await controllerCompleter.future; + final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; // Pump once to trigger the video play. @@ -585,8 +580,8 @@ void main() { // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }); }); @@ -652,7 +647,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -686,7 +682,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -722,7 +718,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -755,13 +752,13 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' + const String getTitleTest = ''' Some title @@ -781,6 +778,7 @@ void main() { textDirection: TextDirection.ltr, child: WebView( initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, @@ -798,13 +796,19 @@ void main() { await pageStarted.future; await pageLoaded.future; + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + final String? title = await controller.getTitle(); expect(title, 'Some title'); }); group('Programmatic Scroll', () { testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' + const String scrollTestPage = ''' @@ -851,7 +855,7 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - await tester.pumpAndSettle(Duration(seconds: 3)); + await tester.pumpAndSettle(const Duration(seconds: 3)); int scrollPosX = await controller.getScrollX(); int scrollPosY = await controller.getScrollY(); @@ -881,9 +885,9 @@ void main() { }); group('NavigationDelegate', () { - final String blankPage = ""; - final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + - base64Encode(const Utf8Encoder().convert(blankPage)); + const String 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 = @@ -912,12 +916,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }); testWidgets('onWebResourceError', (WidgetTester tester) async { @@ -978,7 +981,7 @@ void main() { testWidgets( 'onWebResourceError only called for main frame', (WidgetTester tester) async { - final String iframeTest = ''' + const String iframeTest = ''' @@ -1044,7 +1047,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order @@ -1084,12 +1087,11 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); + expect(currentUrl, secondaryUrl); }); }); @@ -1105,7 +1107,7 @@ void main() { height: 300, child: WebView( key: GlobalKey(), - initialUrl: 'https://flutter.dev/', + initialUrl: primaryUrl, gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); @@ -1116,7 +1118,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); testWidgets('target _blank opens in same window', @@ -1140,14 +1142,12 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); + expect(currentUrl, primaryUrl); }); - // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets( 'can open new window and go back', (WidgetTester tester) async { @@ -1166,27 +1166,27 @@ void main() { onPageFinished: (String url) { pageLoaded.complete(); }, - initialUrl: 'https://flutter.dev', + initialUrl: primaryUrl, ), ), ); final WebViewController controller = await controllerCompleter.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller - .evaluateJavascript('window.open("https://www.google.com/")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); - expect(controller.currentUrl(), completion('https://www.google.com/')); + expect(controller.currentUrl(), completion(secondaryUrl)); expect(controller.canGoBack(), completion(true)); await controller.goBack(); await pageLoaded.future; - expect(controller.currentUrl(), completion('https://flutter.dev/')); + expect(controller.currentUrl(), completion(primaryUrl)); }, - skip: _skipDueToIssue86757, + // Flaky; see https://github.com/flutter/flutter/issues/90976 + skip: true, ); } @@ -1201,13 +1201,108 @@ String _webviewBool(bool value) { /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { - return _evaluateJavascript(controller, 'navigator.userAgent;'); + return await controller.runJavascriptReturningResult('navigator.userAgent;'); } -Future _evaluateJavascript( - WebViewController controller, String js) async { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript(js); +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); } - return jsonDecode(await controller.evaluateJavascript(js)); +} + +class CopyableObjectWithCallback with Copyable { + CopyableObjectWithCallback(this.callback); + + final VoidCallback callback; + + @override + CopyableObjectWithCallback copy() { + return CopyableObjectWithCallback(callback); + } +} + +class ClassWithCallbackClass { + ClassWithCallbackClass() { + callbackClass = CopyableObjectWithCallback( + withWeakRefenceTo( + this, + (WeakReference weakReference) { + return () { + // Weak reference to `this` in callback. + // ignore: unnecessary_statements + weakReference; + }; + }, + ), + ); + } + + late final CopyableObjectWithCallback callbackClass; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile index 66509fcae284..d01e899e347b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index 62428d041adf..1efee8f844ef 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,21 +3,33 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; - 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; }; + 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; }; + 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */; }; + 8FB79B672820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B662820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m */; }; + 8FB79B6928204E8700C101D3 /* FWFPreferencesHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B6828204E8700C101D3 /* FWFPreferencesHostApiTests.m */; }; + 8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B6A28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m */; }; + 8FB79B6D2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B6C2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m */; }; + 8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B72282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m */; }; + 8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B7828209D1300C101D3 /* FWFUserContentControllerHostApiTests.m */; }; + 8FB79B832820A39300C101D3 /* FWFNavigationDelegateHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B822820A39300C101D3 /* FWFNavigationDelegateHostApiTests.m */; }; + 8FB79B852820A3A400C101D3 /* FWFUIDelegateHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B842820A3A400C101D3 /* FWFUIDelegateHostApiTests.m */; }; + 8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B8E2820BAB300C101D3 /* FWFScrollViewHostApiTests.m */; }; + 8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */; }; + 8FB79B972821985200C101D3 /* FWFObjectHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B962821985200C101D3 /* FWFObjectHostApiTests.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 */; }; - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + D7587C3652F6906210B3AE88 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */; }; + DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 572FFC2B2BA326B420B22679 /* libPods-Runner.a */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -52,19 +64,32 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 127772EEA7782174BE0D74B5 /* 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 = ""; }; 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 = ""; }; - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2286ACB87EA8CA27E739AD6C /* 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 = ""; }; + 39B2BDAA45DC06EAB8A6C4E7 /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; + 572FFC2B2BA326B420B22679 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewTests.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 = ""; }; - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = ""; }; + 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = ""; }; + 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFDataConvertersTests.m; sourceTree = ""; }; + 8FB79B662820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFHTTPCookieStoreHostApiTests.m; sourceTree = ""; }; + 8FB79B6828204E8700C101D3 /* FWFPreferencesHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFPreferencesHostApiTests.m; sourceTree = ""; }; + 8FB79B6A28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebsiteDataStoreHostApiTests.m; sourceTree = ""; }; + 8FB79B6C2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewConfigurationHostApiTests.m; sourceTree = ""; }; + 8FB79B72282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFScriptMessageHandlerHostApiTests.m; sourceTree = ""; }; + 8FB79B7828209D1300C101D3 /* FWFUserContentControllerHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFUserContentControllerHostApiTests.m; sourceTree = ""; }; + 8FB79B822820A39300C101D3 /* FWFNavigationDelegateHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFNavigationDelegateHostApiTests.m; sourceTree = ""; }; + 8FB79B842820A3A400C101D3 /* FWFUIDelegateHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFUIDelegateHostApiTests.m; sourceTree = ""; }; + 8FB79B8E2820BAB300C101D3 /* FWFScrollViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFScrollViewHostApiTests.m; sourceTree = ""; }; + 8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFUIViewHostApiTests.m; sourceTree = ""; }; + 8FB79B962821985200C101D3 /* FWFObjectHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFObjectHostApiTests.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; }; @@ -73,12 +98,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 = ""; }; - C475C484BD510DD9CB2E403C /* 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 = ""; }; - E14113434CCE6D3186B5CBC3 /* 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 = ""; }; - F674B2A05DAC369B4FF27850 /* 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 = ""; }; + B89AA31A64040E4A2F1E0CAF /* 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 = ""; }; F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7A1921261392D1CBDAEC2E8 /* 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 */ @@ -86,7 +110,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + D7587C3652F6906210B3AE88 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,7 +118,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,12 +132,33 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 52FBC2B567345431F81A0A0F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 572FFC2B2BA326B420B22679 /* libPods-Runner.a */, + 17781D9462A1AEA7C99F8E45 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { isa = PBXGroup; children = ( - 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, - 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */, + 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */, + 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */, + 8FB79B662820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m */, + 8FB79B6828204E8700C101D3 /* FWFPreferencesHostApiTests.m */, + 8FB79B6A28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m */, + 8FB79B6C2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m */, + 8FB79B72282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m */, + 8FB79B7828209D1300C101D3 /* FWFUserContentControllerHostApiTests.m */, + 8FB79B822820A39300C101D3 /* FWFNavigationDelegateHostApiTests.m */, + 8FB79B842820A3A400C101D3 /* FWFUIDelegateHostApiTests.m */, + 8FB79B8E2820BAB300C101D3 /* FWFScrollViewHostApiTests.m */, + 8FB79B902820BAC700C101D3 /* FWFUIViewHostApiTests.m */, + 8FB79B962821985200C101D3 /* FWFObjectHostApiTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -137,8 +182,8 @@ 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, F7151F75266057800028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, + B8AEEA11D6ECBD09750349AE /* Pods */, + 52FBC2B567345431F81A0A0F /* Frameworks */, ); sourceTree = ""; }; @@ -176,24 +221,15 @@ name = "Supporting Files"; sourceTree = ""; }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { - isa = PBXGroup; - children = ( - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + B8AEEA11D6ECBD09750349AE /* Pods */ = { isa = PBXGroup; children = ( - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + F7A1921261392D1CBDAEC2E8 /* Pods-Runner.debug.xcconfig */, + B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */, + 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */, + 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */, ); - name = Pods; + path = Pods; sourceTree = ""; }; F7151F75266057800028CB91 /* RunnerUITests */ = { @@ -212,7 +248,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + AA38EF430495C2FB50F0F114 /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -231,7 +267,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -273,17 +309,20 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1030; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 68BDCAE823C3F7CB00D9C032 = { + DevelopmentTeam = 7624MWN53C; ProvisioningStyle = Automatic; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; F7151F73266057800028CB91 = { CreatedOnToolsVersion = 12.5; + DevelopmentTeam = 7624MWN53C; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -352,7 +391,7 @@ 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"; }; - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -367,7 +406,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -388,18 +427,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + AA38EF430495C2FB50F0F114 /* [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", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -413,8 +456,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, - 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */, + 8FB79B852820A3A400C101D3 /* FWFUIDelegateHostApiTests.m in Sources */, + 8FB79B972821985200C101D3 /* FWFObjectHostApiTests.m in Sources */, + 8FB79B672820453400C101D3 /* FWFHTTPCookieStoreHostApiTests.m in Sources */, + 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */, + 8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */, + 8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */, + 8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */, + 8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */, + 8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */, + 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */, + 8FB79B6D2820533B00C101D3 /* FWFWebViewConfigurationHostApiTests.m in Sources */, + 8FB79B832820A39300C101D3 /* FWFNavigationDelegateHostApiTests.m in Sources */, + 8FB79B6928204E8700C101D3 /* FWFPreferencesHostApiTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -473,7 +528,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 39B2BDAA45DC06EAB8A6C4E7 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -487,7 +542,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 2286ACB87EA8CA27E739AD6C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -547,7 +602,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -597,7 +652,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d7453a8ce862..cb713d767632 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m index f97b9ef5c8a1..f143297b30d6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m @@ -6,7 +6,7 @@ #import #import "AppDelegate.h" -int main(int argc, char* argv[]) { +int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m deleted file mode 100644 index eb6d1543ec07..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.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 Flutter; -@import XCTest; -@import webview_flutter_wkwebview; - -// OCMock library doesn't generate a valid modulemap. -#import - -@interface FLTWKNavigationDelegateTests : XCTestCase - -@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel; -@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate; - -@end - -@implementation FLTWKNavigationDelegateTests - -- (void)setUp { - self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class); - self.navigationDelegate = - [[FLTWKNavigationDelegate alloc] initWithChannel:self.mockMethodChannel]; -} - -- (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { - if (@available(iOS 9.0, *)) { - // `webViewWebContentProcessDidTerminate` is only available on iOS 9.0 and above. - WKWebView *webview = OCMClassMock(WKWebView.class); - [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; - OCMVerify([self.mockMethodChannel - invokeMethod:@"onWebResourceError" - arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { - XCTAssertEqualObjects(args[@"errorType"], @"webContentProcessTerminated"); - return true; - }]]); - } -} - -@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m deleted file mode 100644 index 631c4a105063..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ /dev/null @@ -1,91 +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 webview_flutter_wkwebview; - -// OCMock library doesn't generate a valid modulemap. -#import - -static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } - -@interface FLTWebViewTests : XCTestCase - -@property(strong, nonatomic) NSObject *mockBinaryMessenger; - -@end - -@implementation FLTWebViewTests - -- (void)setUp { - [super setUp]; - self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); -} - -- (void)testCanInitFLTWebViewController { - FLTWebViewController *controller = - [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) - viewIdentifier:1 - arguments:nil - binaryMessenger:self.mockBinaryMessenger]; - XCTAssertNotNil(controller); -} - -- (void)testCanInitFLTWebViewFactory { - FLTWebViewFactory *factory = - [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; - XCTAssertNotNil(factory); -} - -- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 { - if (@available(iOS 11, *)) { - FLTWebViewController *controller = - [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) - viewIdentifier:1 - arguments:nil - binaryMessenger:self.mockBinaryMessenger]; - UIView *view = controller.view; - XCTAssertTrue([view isKindOfClass:WKWebView.class]); - WKWebView *webView = (WKWebView *)view; - XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, - UIScrollViewContentInsetAdjustmentNever); - } -} - -- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 { - if (@available(iOS 13, *)) { - FLTWebViewController *controller = - [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) - viewIdentifier:1 - arguments:nil - binaryMessenger:self.mockBinaryMessenger]; - UIView *view = controller.view; - XCTAssertTrue([view isKindOfClass:WKWebView.class]); - WKWebView *webView = (WKWebView *)view; - XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); - } -} - -- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { - FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; - 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/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..2ad4bd48b2e8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFInstanceManagerTests.m @@ -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 + +@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); +} +@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..1061abb78f45 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewHostApiTests.m @@ -0,0 +1,411 @@ +// Copyright 2013 The Flutter Authors. 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 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"); +} +@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/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m index d193be745972..689a601d7bdc 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -5,8 +5,27 @@ @import XCTest; @import os.log; +static UIColor *getPixelColorInImage(CGImageRef image, size_t x, size_t y) { + CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image)); + const UInt8 *data = CFDataGetBytePtr(pixelData); + + size_t bytesPerRow = CGImageGetBytesPerRow(image); + size_t pixelInfo = (bytesPerRow * y) + (x * 4); // 4 bytes per pixel + + UInt8 red = data[pixelInfo + 0]; + UInt8 green = data[pixelInfo + 1]; + UInt8 blue = data[pixelInfo + 2]; + UInt8 alpha = data[pixelInfo + 3]; + CFRelease(pixelData); + + return [UIColor colorWithRed:red / 255.0f + green:green / 255.0f + blue:blue / 255.0f + alpha:alpha / 255.0f]; +} + @interface FLTWebViewUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* app; +@property(nonatomic, strong) XCUIApplication *app; @end @implementation FLTWebViewUITests @@ -18,23 +37,71 @@ - (void)setUp { [self.app launch]; } +- (void)testTransparentBackground { + XCUIApplication *app = self.app; + XCUIElement *menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement *transparentBackground = app.buttons[@"Transparent background example"]; + if (![transparentBackground waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Transparent background example"); + } + [transparentBackground tap]; + + XCUIElement *transparentBackgroundLoaded = + app.webViews.staticTexts[@"Transparent background test"]; + if (![transparentBackgroundLoaded waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Transparent background test"); + } + + XCUIScreenshot *screenshot = [[XCUIScreen mainScreen] screenshot]; + + UIImage *screenshotImage = screenshot.image; + CGImageRef screenshotCGImage = screenshotImage.CGImage; + UIColor *centerLeftColor = + getPixelColorInImage(screenshotCGImage, 0, CGImageGetHeight(screenshotCGImage) / 2); + UIColor *centerColor = + getPixelColorInImage(screenshotCGImage, CGImageGetWidth(screenshotCGImage) / 2, + CGImageGetHeight(screenshotCGImage) / 2); + + CGColorSpaceRef centerLeftColorSpace = CGColorGetColorSpace(centerLeftColor.CGColor); + // Flutter Colors.green color : 0xFF4CAF50 -> rgba(76, 175, 80, 1) + // https://github.com/flutter/flutter/blob/f4abaa0735eba4dfd8f33f73363911d63931fe03/packages/flutter/lib/src/material/colors.dart#L1208 + // The background color of the webview is : rgba(0, 0, 0, 0.5) + // The expected color is : rgba(38, 87, 40, 1) + CGFloat flutterGreenColorComponents[] = {38.0f / 255.0f, 87.0f / 255.0f, 40.0f / 255.0f, 1.0f}; + CGColorRef flutterGreenColor = CGColorCreate(centerLeftColorSpace, flutterGreenColorComponents); + CGFloat redColorComponents[] = {1.0f, 0.0f, 0.0f, 1.0f}; + CGColorRef redColor = CGColorCreate(centerLeftColorSpace, redColorComponents); + CGColorSpaceRelease(centerLeftColorSpace); + + XCTAssertTrue(CGColorEqualToColor(flutterGreenColor, centerLeftColor.CGColor)); + XCTAssertTrue(CGColorEqualToColor(redColor, centerColor.CGColor)); +} + - (void)testUserAgent { - XCUIApplication* app = self.app; - XCUIElement* menu = app.buttons[@"Show menu"]; + XCUIApplication *app = self.app; + XCUIElement *menu = app.buttons[@"Show menu"]; if (![menu waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find menu"); } [menu tap]; - XCUIElement* userAgent = app.buttons[@"Show user agent"]; + XCUIElement *userAgent = app.buttons[@"Show user agent"]; if (![userAgent waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Show user agent"); } - NSPredicate* userAgentPredicate = + NSPredicate *userAgentPredicate = [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; - XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCUIElement *userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; XCTAssertFalse(userAgentPopUp.exists); [userAgent tap]; if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { @@ -44,15 +111,15 @@ - (void)testUserAgent { } - (void)testCache { - XCUIApplication* app = self.app; - XCUIElement* menu = app.buttons[@"Show menu"]; + XCUIApplication *app = self.app; + XCUIElement *menu = app.buttons[@"Show menu"]; if (![menu waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find menu"); } [menu tap]; - XCUIElement* clearCache = app.buttons[@"Clear cache"]; + XCUIElement *clearCache = app.buttons[@"Clear cache"]; if (![clearCache waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Clear cache"); @@ -61,21 +128,21 @@ - (void)testCache { [menu tap]; - XCUIElement* listCache = app.buttons[@"List cache"]; + XCUIElement *listCache = app.buttons[@"List cache"]; if (![listCache waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find List cache"); } [listCache tap]; - XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + XCUIElement *emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find empty cache pop up"); } [menu tap]; - XCUIElement* addCache = app.buttons[@"Add to cache"]; + XCUIElement *addCache = app.buttons[@"Add to cache"]; if (![addCache waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find Add to cache"); @@ -89,7 +156,7 @@ - (void)testCache { } [listCache tap]; - XCUIElement* cachePopup = + XCUIElement *cachePopup = app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" @"localStorage\":\"dummy_entry\"}}"]; if (![cachePopup waitForExistenceWithTimeout:30.0]) { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index a953e062ded5..3f61ebfdd6f8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -6,8 +6,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; +import 'dart:io'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'navigation_decision.dart'; @@ -15,7 +18,7 @@ import 'navigation_request.dart'; import 'web_view.dart'; void main() { - runApp(MaterialApp(home: _WebViewExample())); + runApp(const MaterialApp(home: _WebViewExample())); } const String kNavigationExamplePage = ''' @@ -33,6 +36,46 @@ The navigation delegate is set to block navigation to the youtube website. '''; +const String kTransparentBackgroundPage = ''' + + + + Transparent background test + + + +
+

Transparent background test

+
+
+ + +'''; + +const String kLocalFileExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

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

+ + + +'''; + class _WebViewExample extends StatefulWidget { const _WebViewExample({Key? key}) : super(key: key); @@ -44,9 +87,15 @@ class _WebViewExampleState extends State<_WebViewExample> { final Completer _controller = Completer(); + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFF4CAF50), appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. @@ -55,26 +104,23 @@ 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: (context) { - return WebView( - initialUrl: 'https://flutter.dev', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - javascriptChannels: _createJavascriptChannels(context), - javascriptMode: JavascriptMode.unrestricted, - 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; - }, - ); - }), + body: WebView( + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + 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; + }, + backgroundColor: const Color(0x80000000), + ), floatingActionButton: favoriteButton(), ); } @@ -88,8 +134,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')), ); }, @@ -102,7 +147,7 @@ class _WebViewExampleState extends State<_WebViewExample> { } Set _createJavascriptChannels(BuildContext context) { - return { + return { JavascriptChannel( name: 'Snackbar', onMessageReceived: (JavascriptMessage message) { @@ -120,10 +165,16 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, + loadFlutterAsset, + loadLocalFile, + loadHtmlString, + doPostRequest, + setCookie, + transparentBackground, } class _SampleMenu extends StatelessWidget { - _SampleMenu(this.controller); + const _SampleMenu(this.controller); final Future controller; @@ -134,6 +185,7 @@ class _SampleMenu extends StatelessWidget { builder: (BuildContext context, AsyncSnapshot controller) { return PopupMenuButton<_MenuOptions>( + key: const ValueKey('ShowPopupMenu'), onSelected: (_MenuOptions value) { switch (value) { case _MenuOptions.showUserAgent: @@ -157,13 +209,31 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.navigationDelegate: _onNavigationDelegateExample(controller.data!, context); break; + case _MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(controller.data!, context); + break; + case _MenuOptions.loadLocalFile: + _onLoadLocalFileExample(controller.data!, context); + break; + case _MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(controller.data!, context); + break; + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller.data!, context); + break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; + case _MenuOptions.transparentBackground: + _onTransparentBackground(controller.data!, context); + break; } }, 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, @@ -189,24 +259,49 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.navigationDelegate, child: Text('Navigation Delegate example'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), + const PopupMenuItem<_MenuOptions>( + key: ValueKey('ShowTransparentBackgroundExample'), + value: _MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), ], ); }, ); } - void _onShowUserAgent( + Future _onShowUserAgent( WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Snackbar JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } - void _onListCookies( + Future _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, @@ -219,28 +314,32 @@ class _SampleMenu extends StatelessWidget { )); } - void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + Future _onAddToCache( + WebViewController controller, BuildContext context) async { + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), )); } - void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + 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))'); } - void _onClearCache(WebViewController controller, BuildContext context) async { + Future _onClearCache( + WebViewController controller, BuildContext context) async { await controller.clearCache(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("Cache cleared."), + content: Text('Cache cleared.'), )); } - void _onClearCookies( + Future _onClearCookies( WebViewController controller, BuildContext context) async { final bool hadCookies = await WebView.platform.clearCookies(); String message = 'There were cookies. Now, they are gone!'; @@ -252,13 +351,50 @@ class _SampleMenu extends StatelessWidget { )); } - void _onNavigationDelegateExample( + Future _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); await controller.loadUrl('data:text/html;base64,$contentBase64'); } + Future _onLoadFlutterAssetExample( + WebViewController controller, BuildContext context) async { + await controller.loadFlutterAsset('assets/www/index.html'); + } + + Future _onLoadLocalFileExample( + WebViewController controller, BuildContext context) async { + final String pathToIndex = await _prepareLocalFile(); + + await controller.loadFile(pathToIndex); + } + + Future _onLoadHtmlStringExample( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kLocalFileExamplePage); + } + + Future _onDoPostRequest( + WebViewController controller, BuildContext context) async { + final WebViewRequest request = WebViewRequest( + uri: Uri.parse('https://httpbin.org/post'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + await controller.loadRequest(request); + } + + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); @@ -272,6 +408,21 @@ class _SampleMenu extends StatelessWidget { children: cookieWidgets.toList(), ); } + + Future _onTransparentBackground( + WebViewController controller, BuildContext context) async { + await controller.loadHtmlString(kTransparentBackgroundPage); + } + + static Future _prepareLocalFile() async { + final String tmpDir = (await getTemporaryDirectory()).path; + final File indexFile = File('$tmpDir/www/index.html'); + + await Directory('$tmpDir/www').create(recursive: true); + await indexFile.writeAsString(kLocalFileExamplePage); + + return indexFile.path; + } } class _NavigationControls extends StatelessWidget { @@ -289,7 +440,7 @@ class _NavigationControls extends StatelessWidget { final bool webViewReady = snapshot.connectionState == ConnectionState.done; final WebViewController? controller = snapshot.data; - if (controller == null) return Container(); + return Row( children: [ IconButton( @@ -297,12 +448,11 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoBack()) { + if (await controller!.canGoBack()) { await controller.goBack(); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( - const SnackBar(content: Text("No back history item")), + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), ); return; } @@ -313,13 +463,12 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () async { - if (await controller.canGoForward()) { + 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")), + content: Text('No forward history item')), ); return; } @@ -330,7 +479,7 @@ class _NavigationControls extends StatelessWidget { onPressed: !webViewReady ? null : () { - controller.reload(); + controller!.reload(); }, ), ], @@ -340,5 +489,5 @@ class _NavigationControls extends StatelessWidget { } } -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); +/// Callback type for handling messages sent from JavaScript running in a web view. +typedef JavascriptMessageHandler = void Function(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart index c1ff8dc5a690..2f6d7c9f8cdd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart @@ -14,6 +14,6 @@ class NavigationRequest { @override String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + return 'NavigationRequest(url: $url, isForMainFrame: $isForMainFrame)'; } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index ddb8e9b0f14f..c44c4e743669 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -3,10 +3,11 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; -import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; @@ -15,7 +16,7 @@ import 'navigation_request.dart'; /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); +typedef WebViewCreatedCallback = void Function(WebViewController controller); /// Decides how to handle a specific navigation request. /// @@ -23,20 +24,20 @@ typedef void WebViewCreatedCallback(WebViewController controller); /// `navigation` should be handled. /// /// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( +typedef NavigationDelegate = FutureOr Function( NavigationRequest navigation); /// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); +typedef PageStartedCallback = void Function(String url); /// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); +typedef PageFinishedCallback = void Function(String url); /// Signature for when a [WebView] is loading a page. -typedef void PageLoadingCallback(int progress); +typedef PageLoadingCallback = void Function(int progress); /// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); +typedef WebResourceErrorCallback = void Function(WebResourceError error); /// A web view widget for showing html content. /// @@ -54,6 +55,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const [], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -65,9 +67,11 @@ class WebView extends StatefulWidget { this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, + this.zoomEnabled = true, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, + this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), assert(allowsInlineMediaPlayback != null), @@ -93,7 +97,10 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// The initial cookies to set. + final List initialCookies; + + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -166,7 +173,7 @@ class WebView extends StatefulWidget { /// When [onPageFinished] is invoked on Android, the page being rendered may /// not be updated yet. /// - /// When invoked on iOS or Android, any Javascript code that is embedded + /// When invoked on iOS or Android, any JavaScript code that is embedded /// directly in the HTML has been loaded and code injected with /// [WebViewController.evaluateJavascript] can assume this. final PageFinishedCallback? onPageFinished; @@ -213,6 +220,11 @@ class WebView extends StatefulWidget { /// By default `userAgent` is null. final String? userAgent; + /// A Boolean value indicating whether the WebView should support zooming using its on-screen zoom controls and gestures. + /// + /// By default 'zoomEnabled' is true + final bool zoomEnabled; + /// Which restrictions apply on automatic media playback. /// /// This initial value is applied to the platform's webview upon creation. Any following @@ -221,8 +233,14 @@ class WebView extends StatefulWidget { /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + /// The background color of the [WebView]. + /// + /// When `null` the platform's webview default background color is used. By + /// default [backgroundColor] is `null`. + final Color? backgroundColor; + @override - _WebViewState createState() => _WebViewState(); + State createState() => _WebViewState(); } class _WebViewState extends State { @@ -253,7 +271,7 @@ class _WebViewState extends State { context: context, onWebViewPlatformCreated: (WebViewPlatformController? webViewPlatformController) { - WebViewController controller = WebViewController._( + final WebViewController controller = WebViewController._( widget, webViewPlatformController!, _javascriptChannelRegistry, @@ -272,6 +290,8 @@ class _WebViewState extends State { _javascriptChannelRegistry.channels.keys.toSet(), autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, + cookies: widget.initialCookies, + backgroundColor: widget.backgroundColor, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); @@ -299,6 +319,43 @@ class WebViewController { WebView _widget; + /// 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) { + return _webViewPlatformController.loadFlutterAsset(key); + } + + /// 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, + ) { + assert(absoluteFilePath.isNotEmpty); + return _webViewPlatformController.loadFile(absoluteFilePath); + } + + /// 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 _webViewPlatformController.loadHtmlString( + html, + baseUrl: baseUrl, + ); + } + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will @@ -316,6 +373,11 @@ class WebViewController { return _webViewPlatformController.loadUrl(url, headers); } + /// Loads a page by making the specified request. + Future loadRequest(WebViewRequest request) async { + return _webViewPlatformController.loadRequest(request); + } + /// Accessor to the current URL that the WebView is displaying. /// /// If [WebView.initialUrl] was never specified, returns `null`. @@ -410,31 +472,51 @@ class WebViewController { _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); } - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. /// - /// On iOS depending on the value type the return value would be one of: + /// When running JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript + /// embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. /// + /// Depending on the value type the return value would be one of: /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. + /// 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, as well as `undefined` or `null` on iOS 14+. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { + Future runJavascriptReturningResult(String javaScriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); } /// Returns the title of the currently loaded page. @@ -487,7 +569,7 @@ class WebViewController { bool? hasNavigationDelegate; bool? hasProgressTracking; bool? debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); + WebSetting userAgent = const WebSetting.absent(); if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -542,6 +624,7 @@ WebSettings _webSettingsFromWidget(WebView widget) { gestureNavigationEnabled: widget.gestureNavigationEnabled, allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, userAgent: WebSetting.of(widget.userAgent), + zoomEnabled: widget.zoomEnabled, ); } @@ -584,9 +667,28 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } } + @override void onWebResourceError(WebResourceError error) { if (_webView.onWebResourceError != null) { _webView.onWebResourceError!(error); } } } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isIOS) { + WebViewCookieManagerPlatform.instance = WKWebViewCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_wkwebview.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index 229da5e337a5..b8c2464eb051 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -8,6 +8,9 @@ environment: dependencies: flutter: sdk: flutter + + path_provider: ^2.0.6 + webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -18,10 +21,10 @@ dependencies: dev_dependencies: espresso: ^0.1.0+2 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter pedantic: ^1.10.0 @@ -31,3 +34,5 @@ flutter: assets: - assets/sample_audio.ogg - assets/sample_video.mp4 + - assets/www/index.html + - assets/www/styles/style.css 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 8fe331875250..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h +++ /dev/null @@ -1,14 +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 - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m deleted file mode 100644 index f4783ffb4123..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m +++ /dev/null @@ -1,49 +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 "FLTCookieManager.h" - -@implementation FLTCookieManager { -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTCookieManager *instance = [[FLTCookieManager alloc] init]; - - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"clearCookies"]) { - [self clearCookies:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)clearCookies:(FlutterResult)result { - if (@available(iOS 9.0, *)) { - NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; - WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; - - void (^deleteAndNotify)(NSArray *) = - ^(NSArray *cookies) { - BOOL hasCookies = cookies.count > 0; - [dataStore removeDataOfTypes:websiteDataTypes - forDataRecords:cookies - completionHandler:^{ - result(@(hasCookies)); - }]; - }; - - [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; - } else { - // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. - NSLog(@"Clearing cookies is not supported for Flutter WebViews prior to iOS 9."); - } -} - -@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 31edadc8cc05..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h +++ /dev/null @@ -1,21 +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; - -@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 8b7ee7d0cfb7..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m +++ /dev/null @@ -1,116 +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 { - [_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 9f01416acc6a..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,16 +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 { - FLTWebViewFactory* webviewFactory = - [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; ++ (void)registerWithRegistrar:(NSObject *)registrar { + 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"]; - [FLTCookieManager registerWithRegistrar:registrar]; + + // 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..1fe04a39503f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFInstanceManager.m @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter Authors. 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 { + _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..9a8aedd1e646 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m @@ -0,0 +1,284 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#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]; + } + 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 6e795f7d1528..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FLTWebViewController : NSObject - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger; - -- (UIView*)view; -@end - -@interface FLTWebViewFactory : NSObject -- (instancetype)initWithMessenger:(NSObject*)messenger; -@end - -/** - * The WkWebView used for the plugin. - * - * This class overrides some methods in `WKWebView` to serve the needs for the plugin. - */ -@interface FLTWKWebView : WKWebView -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m deleted file mode 100644 index c6d926d3cfc2..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.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 "FlutterWebView.h" -#import "FLTWKNavigationDelegate.h" -#import "FLTWKProgressionDelegate.h" -#import "JavaScriptChannelHandler.h" - -@implementation FLTWebViewFactory { - NSObject* _messenger; -} - -- (instancetype)initWithMessenger:(NSObject*)messenger { - self = [super init]; - if (self) { - _messenger = messenger; - } - return self; -} - -- (NSObject*)createArgsCodec { - return [FlutterStandardMessageCodec sharedInstance]; -} - -- (NSObject*)createWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args { - FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame - viewIdentifier:viewId - arguments:args - binaryMessenger:_messenger]; - return webviewController; -} - -@end - -@implementation FLTWKWebView - -- (void)setFrame:(CGRect)frame { - [super setFrame:frame]; - self.scrollView.contentInset = UIEdgeInsetsZero; - // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of - // webview's contentInsets. - // 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); - } -} - -@end - -@implementation FLTWebViewController { - FLTWKWebView* _webView; - int64_t _viewId; - FlutterMethodChannel* _channel; - NSString* _currentUrl; - // The set of registered JavaScript channel names. - NSMutableSet* _javaScriptChannelNames; - FLTWKNavigationDelegate* _navigationDelegate; - FLTWKProgressionDelegate* _progressionDelegate; -} - -- (instancetype)initWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger { - if (self = [super init]) { - _viewId = viewId; - - NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId]; - _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; - _javaScriptChannelNames = [[NSMutableSet alloc] init]; - - WKUserContentController* userContentController = [[WKUserContentController alloc] init]; - if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { - NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"]; - [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; - [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; - } - - NSDictionary* settings = args[@"settings"]; - - WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; - [self applyConfigurationSettings:settings toConfiguration:configuration]; - configuration.userContentController = userContentController; - [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] - inConfiguration:configuration]; - - _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; - _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; - _webView.UIDelegate = self; - _webView.navigationDelegate = _navigationDelegate; - __weak __typeof__(self) weakSelf = self; - [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { - [weakSelf onMethodCall:call result:result]; - }]; - - if (@available(iOS 11.0, *)) { - _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - if (@available(iOS 13.0, *)) { - _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; - } - } - - [self applySettings:settings]; - // TODO(amirh): return an error if apply settings failed once it's possible to do so. - // https://github.com/flutter/flutter/issues/36228 - - NSString* initialUrl = args[@"initialUrl"]; - if ([initialUrl isKindOfClass:[NSString class]]) { - [self loadUrl:initialUrl]; - } - } - return self; -} - -- (void)dealloc { - if (_progressionDelegate != nil) { - [_progressionDelegate stopObservingProgress:_webView]; - } -} - -- (UIView*)view { - return _webView; -} - -- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([[call method] isEqualToString:@"updateSettings"]) { - [self onUpdateSettings:call result:result]; - } else if ([[call method] isEqualToString:@"loadUrl"]) { - [self onLoadUrl:call result:result]; - } else if ([[call method] isEqualToString:@"canGoBack"]) { - [self onCanGoBack:call result:result]; - } else if ([[call method] isEqualToString:@"canGoForward"]) { - [self onCanGoForward:call result:result]; - } else if ([[call method] isEqualToString:@"goBack"]) { - [self onGoBack:call result:result]; - } else if ([[call method] isEqualToString:@"goForward"]) { - [self onGoForward:call result:result]; - } else if ([[call method] isEqualToString:@"reload"]) { - [self onReload:call result:result]; - } else if ([[call method] isEqualToString:@"currentUrl"]) { - [self onCurrentUrl:call result:result]; - } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { - [self onEvaluateJavaScript:call result:result]; - } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { - [self onAddJavaScriptChannels:call result:result]; - } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { - [self onRemoveJavaScriptChannels:call result:result]; - } else if ([[call method] isEqualToString:@"clearCache"]) { - [self clearCache:result]; - } else if ([[call method] isEqualToString:@"getTitle"]) { - [self onGetTitle:result]; - } else if ([[call method] isEqualToString:@"scrollTo"]) { - [self onScrollTo:call result:result]; - } else if ([[call method] isEqualToString:@"scrollBy"]) { - [self onScrollBy:call result:result]; - } else if ([[call method] isEqualToString:@"getScrollX"]) { - [self getScrollX:call result:result]; - } else if ([[call method] isEqualToString:@"getScrollY"]) { - [self getScrollY:call result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* error = [self applySettings:[call arguments]]; - if (error == nil) { - result(nil); - return; - } - result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); -} - -- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - if (![self loadRequest:[call arguments]]) { - result([FlutterError - errorWithCode:@"loadUrl_failed" - message:@"Failed parsing the URL" - details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); - } else { - result(nil); - } -} - -- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { - BOOL canGoBack = [_webView canGoBack]; - result(@(canGoBack)); -} - -- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { - BOOL canGoForward = [_webView canGoForward]; - result(@(canGoForward)); -} - -- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView goBack]; - result(nil); -} - -- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView goForward]; - result(nil); -} - -- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result { - [_webView reload]; - result(nil); -} - -- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - _currentUrl = [[_webView URL] absoluteString]; - result(_currentUrl); -} - -- (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* jsString = [call arguments]; - if (!jsString) { - result([FlutterError errorWithCode:@"evaluateJavaScript_failed" - message:@"JavaScript String cannot be null" - details:nil]); - return; - } - [_webView evaluateJavaScript:jsString - completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { - if (error) { - result([FlutterError - errorWithCode:@"evaluateJavaScript_failed" - message:@"Failed evaluating JavaScript" - details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@", - jsString, error]]); - } else { - result([NSString stringWithFormat:@"%@", evaluateResult]); - } - }]; -} - -- (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { - NSArray* channelNames = [call arguments]; - NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; - [_javaScriptChannelNames addObjectsFromArray:channelNames]; - [self registerJavaScriptChannels:channelNamesSet - controller:_webView.configuration.userContentController]; - result(nil); -} - -- (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { - // WkWebView does not support removing a single user script, so instead we remove all - // user scripts, all message handlers. And re-register channels that shouldn't be removed. - [_webView.configuration.userContentController removeAllUserScripts]; - for (NSString* channelName in _javaScriptChannelNames) { - [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName]; - } - - NSArray* channelNamesToRemove = [call arguments]; - for (NSString* channelName in channelNamesToRemove) { - [_javaScriptChannelNames removeObject:channelName]; - } - - [self registerJavaScriptChannels:_javaScriptChannelNames - controller:_webView.configuration.userContentController]; - result(nil); -} - -- (void)clearCache:(FlutterResult)result { - if (@available(iOS 9.0, *)) { - NSSet* cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; - WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; - NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; - [dataStore removeDataOfTypes:cacheDataTypes - modifiedSince:dateFrom - completionHandler:^{ - result(nil); - }]; - } else { - // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. - NSLog(@"Clearing cache is not supported for Flutter WebViews prior to iOS 9."); - } -} - -- (void)onGetTitle:(FlutterResult)result { - NSString* title = _webView.title; - result(title); -} - -- (void)onScrollTo:(FlutterMethodCall*)call result:(FlutterResult)result { - NSDictionary* arguments = [call arguments]; - int x = [arguments[@"x"] intValue]; - int y = [arguments[@"y"] intValue]; - - _webView.scrollView.contentOffset = CGPointMake(x, y); - result(nil); -} - -- (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result { - CGPoint contentOffset = _webView.scrollView.contentOffset; - - NSDictionary* arguments = [call arguments]; - int x = [arguments[@"x"] intValue] + contentOffset.x; - int y = [arguments[@"y"] intValue] + contentOffset.y; - - _webView.scrollView.contentOffset = CGPointMake(x, y); - result(nil); -} - -- (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result { - int offsetX = _webView.scrollView.contentOffset.x; - result(@(offsetX)); -} - -- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { - int offsetY = _webView.scrollView.contentOffset.y; - result(@(offsetY)); -} - -// Returns nil when successful, or an error message when one or more keys are unknown. -- (NSString*)applySettings:(NSDictionary*)settings { - NSMutableArray* unknownKeys = [[NSMutableArray alloc] init]; - for (NSString* key in settings) { - if ([key isEqualToString:@"jsMode"]) { - NSNumber* mode = settings[key]; - [self updateJsMode:mode]; - } else if ([key isEqualToString:@"hasNavigationDelegate"]) { - NSNumber* hasDartNavigationDelegate = settings[key]; - _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; - } else if ([key isEqualToString:@"hasProgressTracking"]) { - NSNumber* hasProgressTrackingValue = settings[key]; - bool hasProgressTracking = [hasProgressTrackingValue boolValue]; - if (hasProgressTracking) { - _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView - channel:_channel]; - } - } else if ([key isEqualToString:@"debuggingEnabled"]) { - // no-op debugging is always enabled on iOS. - } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { - NSNumber* allowsBackForwardNavigationGestures = settings[key]; - _webView.allowsBackForwardNavigationGestures = - [allowsBackForwardNavigationGestures boolValue]; - } else if ([key isEqualToString:@"userAgent"]) { - NSString* userAgent = settings[key]; - [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; - } else { - [unknownKeys addObject:key]; - } - } - if ([unknownKeys count] == 0) { - return nil; - } - return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", - [unknownKeys componentsJoinedByString:@", "]]; -} - -- (void)applyConfigurationSettings:(NSDictionary*)settings - toConfiguration:(WKWebViewConfiguration*)configuration { - NSAssert(configuration != _webView.configuration, - @"configuration needs to be updated before webView.configuration."); - for (NSString* key in settings) { - if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { - NSNumber* allowsInlineMediaPlayback = settings[key]; - configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; - } - } -} - -- (void)updateJsMode:(NSNumber*)mode { - WKPreferences* preferences = [[_webView configuration] preferences]; - switch ([mode integerValue]) { - case 0: // disabled - [preferences setJavaScriptEnabled:NO]; - break; - case 1: // unrestricted - [preferences setJavaScriptEnabled:YES]; - break; - default: - NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode); - } -} - -- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy - inConfiguration:(WKWebViewConfiguration*)configuration { - switch ([policy integerValue]) { - case 0: // require_user_action_for_all_media_types - if (@available(iOS 10.0, *)) { - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; - } else if (@available(iOS 9.0, *)) { - configuration.requiresUserActionForMediaPlayback = true; - } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaPlaybackRequiresUserAction = true; -#pragma clang diagnostic pop - } - break; - case 1: // always_allow - if (@available(iOS 10.0, *)) { - configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; - } else if (@available(iOS 9.0, *)) { - configuration.requiresUserActionForMediaPlayback = false; - } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - configuration.mediaPlaybackRequiresUserAction = false; -#pragma clang diagnostic pop - } - break; - default: - NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); - } -} - -- (bool)loadRequest:(NSDictionary*)request { - if (!request) { - return false; - } - - NSString* url = request[@"url"]; - if ([url isKindOfClass:[NSString class]]) { - id headers = request[@"headers"]; - if ([headers isKindOfClass:[NSDictionary class]]) { - return [self loadUrl:url withHeaders:headers]; - } else { - return [self loadUrl:url]; - } - } - - return false; -} - -- (bool)loadUrl:(NSString*)url { - return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; -} - -- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { - NSURL* nsUrl = [NSURL URLWithString:url]; - if (!nsUrl) { - return false; - } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; - [request setAllHTTPHeaderFields:headers]; - [_webView loadRequest:request]; - return true; -} - -- (void)registerJavaScriptChannels:(NSSet*)channelNames - controller:(WKUserContentController*)userContentController { - for (NSString* channelName in channelNames) { - FLTJavaScriptChannel* channel = - [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel - javaScriptChannelName:channelName]; - [userContentController addScriptMessageHandler:channel name:channelName]; - NSString* wrapperSource = [NSString - stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName]; - WKUserScript* wrapperScript = - [[WKUserScript alloc] initWithSource:wrapperSource - injectionTime:WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly:NO]; - [userContentController addUserScript:wrapperScript]; - } -} - -- (void)updateUserAgent:(NSString*)userAgent { - if (@available(iOS 9.0, *)) { - [_webView setCustomUserAgent:userAgent]; - } else { - NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9."); - } -} - -#pragma mark WKUIDelegate - -- (WKWebView*)webView:(WKWebView*)webView - createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration - forNavigationAction:(WKNavigationAction*)navigationAction - windowFeatures:(WKWindowFeatures*)windowFeatures { - if (!navigationAction.targetFrame.isMainFrame) { - [webView loadRequest:navigationAction.request]; - } - - return nil; -} - -@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 new file mode 100644 index 000000000000..1b7eaf646ee9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.modulemap @@ -0,0 +1,10 @@ +framework module webview_flutter_wkwebview { + umbrella header "webview-umbrella.h" + + export * + module * { export * } + + explicit module Test { + 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 a0a5ec657295..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 ec9a363a4b2e..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 new file mode 100644 index 000000000000..dbcd876d15c9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.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 +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec index 37905f147489..479ecf5f256a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -12,12 +12,13 @@ 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/master/packages/webview_flutter/webview_flutter_wkwebview' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_wkwebview' } s.documentation_url = 'https://pub.dev/packages/webview_flutter' - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/FlutterWebView.modulemap' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end 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 new file mode 100644 index 000000000000..2059aa544207 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart @@ -0,0 +1,318 @@ +// Copyright 2013 The Flutter 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 'package:flutter/services.dart'; +import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; + +import '../common/instance_manager.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. +/// +/// Wraps [NSUrlRequest](https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc). +@immutable +class NSUrlRequest { + /// Constructs an [NSUrlRequest]. + const NSUrlRequest({ + required this.url, + this.httpMethod, + this.httpBody, + this.allHttpHeaderFields = const {}, + }); + + /// The URL being requested. + final String url; + + /// The HTTP request method. + /// + /// The default HTTP method is “GET”. + final String? httpMethod; + + /// Data sent as the message body of a request, as in an HTTP POST request. + final Uint8List? httpBody; + + /// 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..b6e4cadec879 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; + +import '../common/instance_manager.dart'; +import '../common/web_kit.pigeon.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/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart new file mode 100644 index 000000000000..a671064ddc0f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -0,0 +1,1058 @@ +// Copyright 2013 The Flutter Authors. 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 '../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. +/// +/// Wraps [WKUserScriptInjectionTime](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc). +enum WKUserScriptInjectionTime { + /// Inject the script after the creation of the webpage’s document element, but before loading any other content. + /// + /// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc. + atDocumentStart, + + /// Inject the script after the document finishes loading, but before loading any other subresources. + /// + /// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentend?language=objc. + atDocumentEnd, +} + +/// The media types that require a user gesture to begin playing. +/// +/// Wraps [WKAudiovisualMediaTypes](https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes?language=objc). +enum WKAudiovisualMediaType { + /// No media types require a user gesture to begin playing. + /// + /// See https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes/wkaudiovisualmediatypenone?language=objc. + none, + + /// Media types that contain audio require a user gesture to begin playing. + /// + /// See https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes/wkaudiovisualmediatypeaudio?language=objc. + audio, + + /// Media types that contain video require a user gesture to begin playing. + /// + /// See https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes/wkaudiovisualmediatypevideo?language=objc. + video, + + /// All media types require a user gesture to begin playing. + /// + /// See https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes/wkaudiovisualmediatypeall?language=objc. + 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). +@immutable +class WKNavigationAction { + /// Constructs a [WKNavigationAction]. + const WKNavigationAction({required this.request, required this.targetFrame}); + + /// The URL request object associated with the navigation action. + final NSUrlRequest request; + + /// The frame in which to display the new content. + final WKFrameInfo targetFrame; +} + +/// An object that contains information about a frame on a webpage. +/// +/// An instance of this class is a transient, data-only object; it does not +/// uniquely identify a frame across multiple delegate method calls. +/// +/// Wraps [WKFrameInfo](https://developer.apple.com/documentation/webkit/wkframeinfo?language=objc). +@immutable +class WKFrameInfo { + /// Construct a [WKFrameInfo]. + const WKFrameInfo({required this.isMainFrame}); + + /// Indicates whether the frame is the web site's main frame or a subframe. + final bool isMainFrame; +} + +/// A script that the web view injects into a webpage. +/// +/// Wraps [WKUserScript](https://developer.apple.com/documentation/webkit/wkuserscript?language=objc). +@immutable +class WKUserScript { + /// Constructs a [UserScript]. + const WKUserScript( + this.source, + this.injectionTime, { + required this.isMainFrameOnly, + }); + + /// The script’s source code. + final String source; + + /// The time at which to inject the script into the webpage. + final WKUserScriptInjectionTime injectionTime; + + /// Indicates whether to inject the script into the main frame or all frames. + final bool isMainFrameOnly; +} + +/// An object that encapsulates a message sent by JavaScript code from a webpage. +/// +/// Wraps [WKScriptMessage](https://developer.apple.com/documentation/webkit/wkscriptmessage?language=objc). +@immutable +class WKScriptMessage { + /// Constructs a [WKScriptMessage]. + const WKScriptMessage({required this.name, this.body}); + + /// The name of the message handler to which the message is sent. + final String name; + + /// The body of the message. + /// + /// Allowed types are [num], [String], [List], [Map], and `null`. + 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.setCookieForInsances(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). +@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. + /// + /// {@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, + ); + } +} + +/// Manages interactions between JavaScript code and your web view. +/// +/// Use this object to do the following: +/// +/// * Inject JavaScript code into webpages running in your web view. +/// * Install custom JavaScript functions that call through to your app’s native +/// code. +/// +/// Wraps [WKUserContentController](https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc). +@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. + /// + /// This name of the parameter must be unique within the user content + /// controller and must not be an empty string. The user content controller + /// uses this parameter to define a JavaScript function for your message + /// handler in the page’s main content world. The name of this function is + /// `window.webkit.messageHandlers..postMessage()`, where + /// `` corresponds to the value of this parameter. For example, if you + /// specify the string `MyFunction`, the user content controller defines the ` + /// `window.webkit.messageHandlers.MyFunction.postMessage()` function in + /// JavaScript. + Future addScriptMessageHandler( + WKScriptMessageHandler handler, + String name, + ) { + assert(name.isNotEmpty); + return _userContentControllerApi.addScriptMessageHandlerForInstances( + this, + handler, + name, + ); + } + + /// Uninstalls the custom message handler with the specified name from your JavaScript code. + /// + /// If no message handler with this name exists in the user content + /// controller, this method does nothing. + /// + /// Use this method to remove a message handler that you previously installed + /// using the [addScriptMessageHandler] method. This method removes the + /// 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) { + return _userContentControllerApi.removeScriptMessageHandlerForInstances( + this, + name, + ); + } + + /// Uninstalls all custom message handlers associated with the user content + /// controller. + /// + /// Only supported on iOS version 14+. + Future removeAllScriptMessageHandlers() { + return _userContentControllerApi.removeAllScriptMessageHandlersForInstances( + this, + ); + } + + /// Injects the specified script into the webpage’s content. + Future addUserScript(WKUserScript userScript) { + return _userContentControllerApi.addUserScriptForInstances( + this, userScript); + } + + /// Removes all user scripts from the web view. + Future removeAllUserScripts() { + 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). +@immutable +class WKWebViewConfiguration extends NSObject { + /// Constructs a [WKWebViewConfiguration]. + 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. + @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 = + 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. + /// + /// 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. + /// + /// Sets [WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1851524-mediatypesrequiringuseractionfor?language=objc). + Future setMediaTypesRequiringUserActionForPlayback( + Set types, + ) { + assert(types.isNotEmpty); + 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). +@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). +@immutable +class WKWebView extends UIView { + /// Constructs a [WKWebView]. + /// + /// [configuration] contains the configuration details for the web view. This + /// method saves a copy of your configuration object. Changes you make to your + /// original object after calling this method have no effect on the web view’s + /// configuration. For a list of configuration options and their default + /// values, see [WKWebViewConfiguration]. If you didn’t create your web view + /// using the `configuration` parameter, this value uses a default + /// configuration object. + 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 + /// view’s configuration. Because this property returns a copy of the + /// configuration object, changes you make to that object don’t affect the web + /// view’s configuration. + /// + /// 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, + 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. + /// + /// 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. + /// + /// 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) { + 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..6a7fb6254889 --- /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 setCookieForInsances( + 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 new file mode 100644 index 000000000000..327210983ae2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart @@ -0,0 +1,713 @@ +// Copyright 2013 The Flutter Authors. 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/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, + required this.onBuildWidget, + this.configuration, + @visibleForTesting this.webViewProxy = const WebViewWidgetProxy(), + }); + + /// The initial parameters used to setup the WebView. + final CreationParams creationParams; + + /// The handler of callbacks made made by [NavigationDelegate]. + final WebViewPlatformCallbacksHandler callbacksHandler; + + /// Manager of named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + /// A collection of properties used to initialize a web view. + /// + /// If null, a default configuration is used. + final WKWebViewConfiguration? configuration; + + /// The handler for constructing [WKWebView]s and calling static methods. + /// + /// This should only be changed for testing purposes. + final WebViewWidgetProxy webViewProxy; + + /// A callback to build a widget once [WKWebView] has been initialized. + final Widget Function(WebKitWebViewPlatformController controller) + onBuildWidget; + + @override + State createState() => _WebKitWebViewWidgetState(); +} + +class _WebKitWebViewWidgetState extends State { + late final WebKitWebViewPlatformController controller; + + @override + void initState() { + super.initState(); + controller = WebKitWebViewPlatformController( + creationParams: widget.creationParams, + callbacksHandler: widget.callbacksHandler, + javascriptChannelRegistry: widget.javascriptChannelRegistry, + configuration: widget.configuration, + webViewProxy: widget.webViewProxy, + ); + } + + @override + Widget build(BuildContext context) { + return widget.onBuildWidget(controller); + } +} + +/// An implementation of [WebViewPlatformController] with the WebKit api. +class WebKitWebViewPlatformController extends WebViewPlatformController { + /// Construct a [WebKitWebViewPlatformController]. + WebKitWebViewPlatformController({ + required CreationParams creationParams, + required this.callbacksHandler, + required this.javascriptChannelRegistry, + WKWebViewConfiguration? configuration, + @visibleForTesting this.webViewProxy = const WebViewWidgetProxy(), + }) : super(callbacksHandler) { + _setCreationParams( + creationParams, + configuration: configuration ?? WKWebViewConfiguration(), + ); + } + + bool _zoomEnabled = true; + bool _hasNavigationDelegate = false; + bool _progressObserverSet = false; + + final Map _scriptMessageHandlers = + {}; + + /// Handles callbacks that are made by navigation. + final WebViewPlatformCallbacksHandler callbacksHandler; + + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + /// Handles constructing a [WKWebView]. + /// + /// This should only be changed when used for testing. + final WebViewWidgetProxy webViewProxy; + + /// Represents the WebView maintained by platform code. + late final WKWebView webView; + + /// Used to integrate custom user interface elements into web view interactions. + @visibleForTesting + 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, { + required WKWebViewConfiguration configuration, + }) async { + _setWebViewConfiguration( + configuration, + allowsInlineMediaPlayback: params.webSettings?.allowsInlineMediaPlayback, + autoMediaPlaybackPolicy: params.autoMediaPlaybackPolicy, + ); + + 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( + WKWebViewConfiguration configuration, { + required bool? allowsInlineMediaPlayback, + required AutoMediaPlaybackPolicy autoMediaPlaybackPolicy, + }) { + if (allowsInlineMediaPlayback != null) { + configuration.setAllowsInlineMediaPlayback(allowsInlineMediaPlayback); + } + + late final bool requiresUserAction; + switch (autoMediaPlaybackPolicy) { + case AutoMediaPlaybackPolicy.require_user_action_for_all_media_types: + requiresUserAction = true; + break; + case AutoMediaPlaybackPolicy.always_allow: + requiresUserAction = false; + break; + } + + 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 + Future addJavascriptChannels(Set javascriptChannelNames) async { + await Future.wait( + javascriptChannelNames.where( + (String channelName) { + return !_scriptMessageHandlers.containsKey(channelName); + }, + ).map>( + (String channelName) { + final WKScriptMessageHandler handler = + webViewProxy.createScriptMessageHandler( + didReceiveScriptMessage: withWeakRefenceTo( + javascriptChannelRegistry, + (WeakReference weakReference) { + return ( + WKUserContentController userContentController, + WKScriptMessage message, + ) { + weakReference.target?.onJavascriptChannelMessage( + message.name, + message.body!.toString(), + ); + }; + }, + ), + ); + _scriptMessageHandlers[channelName] = handler; + + final String wrapperSource = + 'window.$channelName = webkit.messageHandlers.$channelName;'; + final WKUserScript wrapperScript = WKUserScript( + wrapperSource, + WKUserScriptInjectionTime.atDocumentStart, + isMainFrameOnly: false, + ); + webView.configuration.userContentController + .addUserScript(wrapperScript); + return webView.configuration.userContentController + .addScriptMessageHandler( + handler, + channelName, + ); + }, + ), + ); + } + + @override + Future removeJavascriptChannels( + Set javascriptChannelNames, + ) async { + if (javascriptChannelNames.isEmpty) { + return; + } + + 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(); + // 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, + ); + + removedJavaScriptChannels.forEach(_scriptMessageHandlers.remove); + final Set remainingNames = _scriptMessageHandlers.keys.toSet(); + _scriptMessageHandlers.clear(); + + 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(); + } +} + +/// Handles constructing objects and calling static methods. +/// +/// This should only be used for testing purposes. +@visibleForTesting +class WebViewWidgetProxy { + /// Constructs a [WebViewWidgetProxy]. + const WebViewWidgetProxy(); + + /// Constructs a [WKWebView]. + WKWebView createWebView( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) { + return WKWebView(configuration, observeValue: observeValue); + } + + /// Constructs a [WKScriptMessageHandler]. + WKScriptMessageHandler createScriptMessageHandler({ + required void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ) + didReceiveScriptMessage, + }) { + return WKScriptMessageHandler( + didReceiveScriptMessage: didReceiveScriptMessage, + ); + } + + /// Constructs a [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 05b79d0a72e4..f046ea4378b8 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 @@ -9,6 +9,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart'; + +import 'foundation/foundation.dart'; /// Builds an iOS webview. /// @@ -25,25 +28,33 @@ 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(), ); } @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } } 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 new file mode 100644 index 000000000000..59c9f580db74 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.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: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'; + +/// 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() async { + return 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 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/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart index bbec415dccd0..f647ab38a41b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart @@ -3,3 +3,4 @@ // found in the LICENSE file. export 'src/webview_cupertino.dart'; +export 'src/wkwebview_cookie_manager.dart'; 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 c6f6d6f94f07..cd92b8625105 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -1,12 +1,12 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. -repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview +repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.13 +version: 2.9.0 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: @@ -18,12 +18,15 @@ flutter: dependencies: flutter: sdk: flutter - - webview_flutter_platform_interface: ^1.0.0 + path: ^1.8.0 + webview_flutter_platform_interface: ^1.8.0 dev_dependencies: + build_runner: ^2.1.5 flutter_driver: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.10.0 \ No newline at end of file + mockito: ^5.1.0 + pedantic: ^1.10.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 new file mode 100644 index 000000000000..c6d90d04e35e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart @@ -0,0 +1,1263 @@ +// Copyright 2013 The Flutter Authors. 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#106316) +// 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/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/web_kit/web_kit.dart'; +import 'package:webview_flutter_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, + WebViewPlatformCallbacksHandler, + WebViewWidgetProxy, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + 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; + + late WebKitWebViewPlatformController testController; + + setUp(() { + mockWebView = MockWKWebView(); + mockWebViewConfiguration = MockWKWebViewConfiguration(); + mockUserContentController = MockWKUserContentController(); + mockPreferences = MockWKPreferences(); + mockUIDelegate = MockWKUIDelegate(); + mockScrollView = MockUIScrollView(); + mockWebsiteDataStore = MockWKWebsiteDataStore(); + mockNavigationDelegate = MockWKNavigationDelegate(); + mockWebViewWidgetProxy = MockWebViewWidgetProxy(); + + 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(); + }); + + // Builds a WebViewCupertinoWidget with default parameters. + Future buildWidget( + WidgetTester tester, { + CreationParams? creationParams, + bool hasNavigationDelegate = false, + bool hasProgressTracking = false, + }) async { + await tester.pumpWidget(WebKitWebViewWidget( + creationParams: creationParams ?? + CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + )), + callbacksHandler: mockCallbacksHandler, + javascriptChannelRegistry: mockJavascriptChannelRegistry, + webViewProxy: mockWebViewWidgetProxy, + configuration: mockWebViewConfiguration, + onBuildWidget: (WebKitWebViewPlatformController controller) { + testController = controller; + return Container(); + }, + )); + await tester.pumpAndSettle(); + } + + testWidgets('build $WebKitWebViewWidget', (WidgetTester tester) async { + await buildWidget(tester); + }); + + testWidgets('Requests to open a new window loads request in same window', + (WidgetTester tester) async { + await buildWidget(tester); + + 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, + targetFrame: WKFrameInfo(isMainFrame: false), + ), + ); + + verify(mockWebView.loadRequest(request)); + }); + + 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, + ), + ), + ); + + verify(mockWebViewConfiguration + .setMediaTypesRequiringUserActionForPlayback(< + WKAudiovisualMediaType>{ + WKAudiovisualMediaType.all, + })); + }); + + testWidgets('autoMediaPlaybackPolicy false', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + autoMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + verify(mockWebViewConfiguration + .setMediaTypesRequiringUserActionForPlayback(< + WKAudiovisualMediaType>{ + WKAudiovisualMediaType.none, + })); + }); + + testWidgets('javascriptChannelNames', (WidgetTester tester) async { + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget( + tester, + creationParams: CreationParams( + javascriptChannelNames: {'a', 'b'}, + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + hasNavigationDelegate: false, + ), + ), + ); + + final List javaScriptChannels = verify( + mockUserContentController.addScriptMessageHandler( + captureAny, + captureAny, + ), + ).captured; + expect( + javaScriptChannels[0], + isA(), + ); + expect(javaScriptChannels[1], 'a'); + expect( + javaScriptChannels[2], + isA(), + ); + expect(javaScriptChannels[3], 'b'); + }); + + 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, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: const WebSetting.absent(), + allowsInlineMediaPlayback: true, + ), + ), + ); + + verify(mockWebViewConfiguration.setAllowsInlineMediaPlayback(true)); + }); + }); + }); + + 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(null), + ); + 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( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget(tester); + + await testController.addJavascriptChannels({'c', 'd'}); + final List javaScriptChannels = verify( + mockUserContentController.addScriptMessageHandler( + captureAny, captureAny), + ).captured; + expect( + javaScriptChannels[0], + isA(), + ); + expect(javaScriptChannels[1], 'c'); + expect( + javaScriptChannels[2], + isA(), + ); + expect(javaScriptChannels[3], 'd'); + + final List userScripts = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .cast(); + expect(userScripts[0].source, 'window.c = webkit.messageHandlers.c;'); + expect( + userScripts[0].injectionTime, + WKUserScriptInjectionTime.atDocumentStart, + ); + expect(userScripts[0].isMainFrameOnly, false); + expect(userScripts[1].source, 'window.d = webkit.messageHandlers.d;'); + expect( + userScripts[1].injectionTime, + WKUserScriptInjectionTime.atDocumentStart, + ); + expect(userScripts[0].isMainFrameOnly, false); + }); + + testWidgets('removeJavascriptChannels', (WidgetTester tester) async { + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget(tester); + + await testController.addJavascriptChannels({'c', 'd'}); + reset(mockUserContentController); + + await testController.removeJavascriptChannels({'c'}); + + verify(mockUserContentController.removeAllUserScripts()); + verify(mockUserContentController.removeScriptMessageHandler('c')); + verify(mockUserContentController.removeScriptMessageHandler('d')); + + final List javaScriptChannels = verify( + mockUserContentController.addScriptMessageHandler( + captureAny, + captureAny, + ), + ).captured; + expect( + javaScriptChannels[0], + isA(), + ); + expect(javaScriptChannels[1], 'd'); + + final List userScripts = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .cast(); + expect(userScripts[0].source, 'window.d = webkit.messageHandlers.d;'); + expect( + userScripts[0].injectionTime, + WKUserScriptInjectionTime.atDocumentStart, + ); + 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('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, hasProgressTracking: false); + + verifyNever(mockWebView.removeObserver( + mockWebView, + keyPath: 'estimatedProgress', + )); + }); + }); + + group('JavascriptChannelRegistry', () { + testWidgets('onJavascriptChannelMessage', (WidgetTester tester) async { + when( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: anyNamed('didReceiveScriptMessage'), + ), + ).thenReturn( + MockWKScriptMessageHandler(), + ); + + await buildWidget(tester); + await testController.addJavascriptChannels({'hello'}); + + final dynamic didReceiveScriptMessage = verify( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: + captureAnyNamed('didReceiveScriptMessage'))) + .captured + .single as void Function( + WKUserContentController userContentController, + WKScriptMessage message, + ); + + didReceiveScriptMessage( + mockUserContentController, + const WKScriptMessage(name: 'hello', body: 'A message.'), + ); + verify(mockJavascriptChannelRegistry.onJavascriptChannelMessage( + 'hello', + 'A 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 new file mode 100644 index 000000000000..f216711ca0b2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart @@ -0,0 +1,648 @@ +// 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 _i5; +import 'dart:math' as _i2; +import 'dart:ui' as _i6; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart' + as _i9; +import 'package:webview_flutter_platform_interface/src/types/types.dart' + as _i10; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + as _i8; +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; +import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart' + 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 +// 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 _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 _FakeWKUIDelegate_10 extends _i1.Fake implements _i4.WKUIDelegate {} + +/// 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); +} + +/// 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_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 _i4.WKScriptMessageHandler { + MockWKScriptMessageHandler() { + _i1.throwOnMissingStub(this); + } + + @override + 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. +// 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_5()) + 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_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 _i4.WKWebViewConfiguration { + MockWKWebViewConfiguration() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKUserContentController get userContentController => + (super.noSuchMethod(Invocation.getter(#userContentController), + returnValue: _FakeWKUserContentController_7()) + as _i4.WKUserContentController); + @override + _i4.WKPreferences get preferences => + (super.noSuchMethod(Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_3()) as _i4.WKPreferences); + @override + _i4.WKWebsiteDataStore get websiteDataStore => + (super.noSuchMethod(Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_8()) 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_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. +// ignore: must_be_immutable +class MockWKUIDelegate extends _i1.Mock implements _i4.WKUIDelegate { + MockWKUIDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _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 _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_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 _i8.JavascriptChannelRegistry { + MockJavascriptChannelRegistry() { + _i1.throwOnMissingStub(this); + } + + @override + Map get channels => + (super.noSuchMethod(Invocation.getter(#channels), + returnValue: {}) + as Map); + @override + void onJavascriptChannelMessage(String? channel, String? message) => + super.noSuchMethod( + Invocation.method(#onJavascriptChannelMessage, [channel, message]), + returnValueForMissingStub: null); + @override + void updateJavascriptChannelsFromSet(Set<_i9.JavascriptChannel>? channels) => + super.noSuchMethod( + 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 _i8.WebViewPlatformCallbacksHandler { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + (super.noSuchMethod( + Invocation.method(#onNavigationRequest, [], + {#url: url, #isForMainFrame: isForMainFrame}), + returnValue: 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(_i10.WebResourceError? error) => + super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), + returnValueForMissingStub: null); +} + +/// A class which mocks [WebViewWidgetProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewWidgetProxy extends _i1.Mock + implements _i11.WebViewWidgetProxy { + MockWebViewWidgetProxy() { + _i1.throwOnMissingStub(this); + } + + @override + _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 + _i4.WKScriptMessageHandler createScriptMessageHandler( + {void Function(_i4.WKUserContentController, _i4.WKScriptMessage)? + didReceiveScriptMessage}) => + (super.noSuchMethod( + Invocation.method(#createScriptMessageHandler, [], + {#didReceiveScriptMessage: didReceiveScriptMessage}), + returnValue: _FakeWKScriptMessageHandler_4()) + as _i4.WKScriptMessageHandler); + @override + _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/wifi_info_flutter/analysis_options.yaml b/packages/wifi_info_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/wifi_info_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/wifi_info_flutter/wifi_info_flutter/.metadata b/packages/wifi_info_flutter/wifi_info_flutter/.metadata deleted file mode 100644 index e40242b8f94f..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: 4513e96a3022d70aa7686906c2e9bdfbbc448334 - channel: master - -project_type: plugin diff --git a/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS b/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# 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/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md deleted file mode 100644 index 86f3f67af103..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ /dev/null @@ -1,37 +0,0 @@ -## NEXT - -* Updated Android lint settings. - -## 2.0.2 - -* Update README to point to Plus Plugins version. - -## 2.0.1 - -* Migrate maven repository from jcenter to mavenCentral. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.4 - -* Android: Add Log warning for unsatisfied requirement(s) in Android P or higher. -* Android: Update Example project. - -## 1.0.3 - -* Fix README example. - -## 1.0.2 - -* Update Flutter SDK constraint. - -## 1.0.1 - -* Fixed method channel name in android implementation. [Issue](https://github.com/flutter/flutter/issues/69073). - -## 1.0.0 - -* Initial release of the plugin. This plugin retrieves information about a device's connection to wifi. -* See [README](./README.md) for details. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/README.md b/packages/wifi_info_flutter/wifi_info_flutter/README.md deleted file mode 100644 index 3f1084eb1d2c..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# wifi_info_flutter - ---- - -## Deprecation Notice - -This plugin has been replaced by the [Flutter Community Plus -Plugins](https://plus.fluttercommunity.dev/) version, -[`network_info_plus`](https://pub.dev/packages/network_info_plus). -No further updates are planned to this plugin, and we encourage all users to -migrate to the Plus version. - -Critical fixes (e.g., for any security incidents) will be provided through the -end of 2021, at which point this package will be marked as discontinued. - ---- - -This plugin retrieves information about a device's connection to wifi. - -> Note that on Android, this does not guarantee connection to Internet. For instance, -the app might have wifi access but it might be a VPN or a hotel WiFi with no access. - -## Usage - -### Android - -Sample usage to check current status: - -To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, ensure all of the following conditions are met: - - * If your app is targeting Android 10 (API level 29) SDK or higher, your app has the ACCESS_FINE_LOCATION permission. - - * If your app is targeting SDK lower than Android 10 (API level 29), your app has the ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission. - - * Location services are enabled on the device (under Settings > Location). - -You can get wi-fi related information using: - -```dart -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; - -var wifiBSSID = await WifiInfo().getWifiBSSID(); -var wifiIP = await WifiInfo().getWifiIP(); -var wifiName = await WifiInfo().getWifiName(); -``` - -### iOS 12 - -To use `.getWifiBSSID()` and `.getWifiName()` on iOS >= 12, the `Access WiFi information capability` in XCode must be enabled. Otherwise, both methods will return null. - -### iOS 13 - -The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [`CNCopyCurrentNetworkInfo`](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS. - -As of iOS 13, Apple announced that these APIs will no longer return valid information. -An app linked against iOS 12 or earlier receives pseudo-values such as: - - * SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU). - - * BSSID: "00:00:00:00:00:00" - -An app linked against iOS 13 or later receives `null`. - -The `CNCopyCurrentNetworkInfo` will work for Apps that: - - * The app uses Core Location, and has the user’s authorization to use location information. - - * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. - - * The app has active VPN configurations installed. - -If your app falls into the last two categories, it will work as it is. If your app doesn't fall into the last two categories, -and you still need to access the wifi information, you should request user's authorization to use location information. - -There is a helper method provided in this plugin to request the location authorization: `requestLocationServiceAuthorization`. -To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: - -* `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. -* `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). - -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle deleted file mode 100644 index 661ee82da4d0..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -group 'io.flutter.plugins.wifi_info_flutter' -version '1.0' - -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - disable 'GradleDependency' - } - - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties b/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 01a286e96a21..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -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 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle deleted file mode 100644 index ec0e24958ea9..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'wifi_info_flutter' diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml deleted file mode 100644 index 03ac924f9427..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java deleted file mode 100644 index bd4c8f10ce3b..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java +++ /dev/null @@ -1,157 +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.wifi_info_flutter; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.LocationManager; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.os.Build; -import androidx.core.content.ContextCompat; -import io.flutter.Log; - -/** Reports wifi information. */ -class WifiInfoFlutter { - private WifiManager wifiManager; - private Context context; - private static final String TAG = "WifiInfoFlutter"; - - WifiInfoFlutter(WifiManager wifiManager, Context context) { - this.wifiManager = wifiManager; - this.context = context; - } - - String getWifiName() { - if (!checkPermissions()) { - return null; - } - final WifiInfo wifiInfo = getWifiInfo(); - String ssid = null; - if (wifiInfo != null) ssid = wifiInfo.getSSID(); - if (ssid != null) ssid = ssid.replaceAll("\"", ""); // Android returns "SSID" - if (ssid != null && ssid.equals("")) ssid = null; - return ssid; - } - - String getWifiBSSID() { - if (!checkPermissions()) { - return null; - } - final WifiInfo wifiInfo = getWifiInfo(); - String bssid = null; - if (wifiInfo != null) { - bssid = wifiInfo.getBSSID(); - } - return bssid; - } - - String getWifiIPAddress() { - WifiInfo wifiInfo = null; - if (wifiManager != null) wifiInfo = wifiManager.getConnectionInfo(); - - String ip = null; - int i_ip = 0; - if (wifiInfo != null) i_ip = wifiInfo.getIpAddress(); - - if (i_ip != 0) - ip = - String.format( - "%d.%d.%d.%d", - (i_ip & 0xff), (i_ip >> 8 & 0xff), (i_ip >> 16 & 0xff), (i_ip >> 24 & 0xff)); - - return ip; - } - - private WifiInfo getWifiInfo() { - return wifiManager == null ? null : wifiManager.getConnectionInfo(); - } - - private Boolean checkPermissions() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return true; - } - - boolean grantedChangeWifiState = - ContextCompat.checkSelfPermission(context, Manifest.permission.CHANGE_WIFI_STATE) - == PackageManager.PERMISSION_GRANTED; - - boolean grantedAccessFine = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED; - - boolean grantedAccessCoarse = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P - && !grantedChangeWifiState - && !grantedAccessFine - && !grantedAccessCoarse) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, please ensure your app has one of the following permissions:\n" - + "- CHANGE_WIFI_STATE\n" - + "- ACCESS_FINE_LOCATION\n" - + "- ACCESS_COARSE_LOCATION\n" - + "For more information about Wi-Fi Restrictions in Android 8.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && !grantedChangeWifiState) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure your app has the CHANGE_WIFI_STATE permission.\n" - + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P - && !grantedAccessFine - && !grantedAccessCoarse) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, additional to CHANGE_WIFI_STATE please ensure your app has one of the following permissions too:\n" - + "- ACCESS_FINE_LOCATION\n" - + "- ACCESS_COARSE_LOCATION\n" - + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && (!grantedAccessFine || !grantedChangeWifiState)) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android Q, please ensure your app has the CHANGE_WIFI_STATE and ACCESS_FINE_LOCATION permission.\n" - + "For more information about Wi-Fi Restrictions in Android 10.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - - LocationManager locationManager = - (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - - boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !gpsEnabled) { - Log.w( - TAG, - "Attempted to get Wi-Fi data that requires additional permission(s).\n" - + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure Location services are enabled on the device (under Settings > Location).\n" - + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" - + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); - return false; - } - return true; - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java deleted file mode 100644 index 9ceed5968e63..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java +++ /dev/null @@ -1,44 +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.wifi_info_flutter; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -/** - * The handler receives {@link MethodCall}s from the UIThread, gets the related information from - * a @{@link WifiInfoFlutter}, and then send the result back to the UIThread through the {@link - * MethodChannel.Result}. - */ -class WifiInfoFlutterMethodChannelHandler implements MethodChannel.MethodCallHandler { - private WifiInfoFlutter wifiInfoFlutter; - - /** - * Construct the WifiInfoFlutterMethodChannelHandler with a {@code wifiInfoFlutter}. The {@code - * wifiInfoFlutter} must not be null. - */ - WifiInfoFlutterMethodChannelHandler(WifiInfoFlutter wifiInfoFlutter) { - assert (wifiInfoFlutter != null); - this.wifiInfoFlutter = wifiInfoFlutter; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case "wifiName": - result.success(wifiInfoFlutter.getWifiName()); - break; - case "wifiBSSID": - result.success(wifiInfoFlutter.getWifiBSSID()); - break; - case "wifiIPAddress": - result.success(wifiInfoFlutter.getWifiIPAddress()); - break; - default: - result.notImplemented(); - break; - } - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java deleted file mode 100644 index 7757688bc9fa..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java +++ /dev/null @@ -1,46 +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.wifi_info_flutter; - -import android.content.Context; -import android.net.wifi.WifiManager; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; - -/** WifiInfoFlutterPlugin */ -public class WifiInfoFlutterPlugin implements FlutterPlugin { - private MethodChannel methodChannel; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - WifiInfoFlutterPlugin plugin = new WifiInfoFlutterPlugin(); - plugin.setupChannels(registrar.messenger(), registrar.context()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - setupChannels(binding.getBinaryMessenger(), binding.getApplicationContext()); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - - private void setupChannels(BinaryMessenger messenger, Context context) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/wifi_info_flutter"); - final WifiManager wifiManager = - (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - - final WifiInfoFlutter wifiInfoFlutter = new WifiInfoFlutter(wifiManager, context); - - final WifiInfoFlutterMethodChannelHandler methodChannelHandler = - new WifiInfoFlutterMethodChannelHandler(wifiInfoFlutter); - methodChannel.setMethodCallHandler(methodChannelHandler); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata b/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata deleted file mode 100644 index 8407594c70b4..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: 4513e96a3022d70aa7686906c2e9bdfbbc448334 - channel: master - -project_type: app diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/README.md b/packages/wifi_info_flutter/wifi_info_flutter/example/README.md deleted file mode 100644 index 30c38ad1ba92..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# wifi_info_flutter_example - -Demonstrates how to use the wifi_info_flutter 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/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle deleted file mode 100644 index 86cf517168ef..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle +++ /dev/null @@ -1,54 +0,0 @@ -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 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.flutter.plugins.wifi_info_flutter_example" - minSdkVersion 16 - targetSdkVersion 29 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - 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 '../..' -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 357602a1d503..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index bcecab36d14a..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java deleted file mode 100644 index b52123be65d4..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java +++ /dev/null @@ -1,9 +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.wifi_info_flutter_example; - -import io.flutter.embedding.android.FlutterActivity; - -public class MainActivity extends FlutterActivity {} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 357602a1d503..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle deleted file mode 100644 index 456d020f6e2c..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -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/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 296b146b7318..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Jun 23 08:50:38 CEST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_test.dart deleted file mode 100644 index 8190062e3ebd..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_test.dart +++ /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. - -import 'dart:io'; -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('$WifiInfo test driver', () { - late WifiInfo _wifiInfo; - - setUpAll(() async { - _wifiInfo = WifiInfo(); - }); - - testWidgets('test location methods, iOS only', (WidgetTester tester) async { - expect( - (await _wifiInfo.getLocationServiceAuthorization()), - LocationAuthorizationStatus.notDetermined, - ); - }, skip: !Platform.isIOS); - }); -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index f2872cf474ee..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 9.0 - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile deleted file mode 100644 index 07a4e08abf54..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# 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__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - # Work around https://github.com/flutter/flutter/issues/82964. - if target.name == 'Reachability' - target.build_configurations.each do |config| - config.build_settings['WARNING_CFLAGS'] = '-Wno-pointer-to-int-cast' - end - end - flutter_additional_ios_build_settings(target) - end -end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 98b6089f0d68..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,545 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 78A2B22AE5FB53474D0E7B48 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5304CE43F05781426D604828 /* 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 */; }; -/* 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 */ - 11F3C35054888B3724893A22 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 1E5A9EB282D46A445314F9FD /* 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 = ""; }; - 2BADCFEAF6163E1D252C8765 /* 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 = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 5304CE43F05781426D604828 /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 78A2B22AE5FB53474D0E7B48 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 53534922C743E29B902DE7D2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5304CE43F05781426D604828 /* libPods-Runner.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 */, - 97C146EF1CF9000F007C117D /* Products */, - 9938C0E7E0C974A660788A69 /* Pods */, - 53534922C743E29B902DE7D2 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - 9938C0E7E0C974A660788A69 /* Pods */ = { - isa = PBXGroup; - children = ( - 1E5A9EB282D46A445314F9FD /* Pods-Runner.debug.xcconfig */, - 2BADCFEAF6163E1D252C8765 /* Pods-Runner.release.xcconfig */, - 11F3C35054888B3724893A22 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 77C985AE0A130001A78D36BE /* [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 = 1020; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - 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; - 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"; - }; - 77C985AE0A130001A78D36BE /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - 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 = 9.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - 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 = 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_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; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a28140cfdb3f..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 442514aaecbe..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m +++ /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 "AppDelegate.h" -#import "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/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2d9d..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist deleted file mode 100644 index 2c7f6b8c6b85..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - wifi_info_flutter_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 - - NSLocationAlwaysAndWhenInUseUsageDescription - This app requires accessing your location information all the time to get wi-fi information. - NSLocationWhenInUseUsageDescription - This app requires accessing your location information when the app is in foreground to get wi-fi information. - - diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/main.m b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/main.m deleted file mode 100644 index f97b9ef5c8a1..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart deleted file mode 100644 index 8258815b0c09..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart +++ /dev/null @@ -1,173 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:connectivity/connectivity.dart' - show Connectivity, ConnectivityResult; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:wifi_info_flutter/wifi_info_flutter.dart'; - -// Sets a platform override for desktop to avoid exceptions. See -// https://flutter.dev/desktop#target-platform-override for more info. -void _enablePlatformOverrideForDesktop() { - if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { - debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; - } -} - -void main() { - _enablePlatformOverrideForDesktop(); - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String _connectionStatus = 'Unknown'; - final Connectivity _connectivity = Connectivity(); - final WifiInfo _wifiInfo = WifiInfo(); - late StreamSubscription _connectivitySubscription; - - @override - void initState() { - super.initState(); - initConnectivity(); - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); - } - - @override - void dispose() { - _connectivitySubscription.cancel(); - super.dispose(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initConnectivity() async { - late ConnectivityResult result; - // Platform messages may fail, so we use a try/catch PlatformException. - try { - result = await _connectivity.checkConnectivity(); - } on PlatformException catch (e) { - print(e.toString()); - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) { - return Future.value(null); - } - - return _updateConnectionStatus(result); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Connectivity example app'), - ), - body: Center(child: Text('Connection Status: $_connectionStatus')), - ); - } - - Future _updateConnectionStatus(ConnectivityResult result) async { - switch (result) { - case ConnectivityResult.wifi: - String? wifiName, wifiBSSID, wifiIP; - - try { - if (!kIsWeb && Platform.isIOS) { - LocationAuthorizationStatus status = - await _wifiInfo.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = await _wifiInfo.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiName = await _wifiInfo.getWifiName(); - } else { - wifiName = await _wifiInfo.getWifiName(); - } - } else { - wifiName = await _wifiInfo.getWifiName(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiName = "Failed to get Wifi Name"; - } - - try { - if (!kIsWeb && Platform.isIOS) { - LocationAuthorizationStatus status = - await _wifiInfo.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = await _wifiInfo.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiBSSID = await _wifiInfo.getWifiBSSID(); - } else { - wifiBSSID = await _wifiInfo.getWifiBSSID(); - } - } else { - wifiBSSID = await _wifiInfo.getWifiBSSID(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiBSSID = "Failed to get Wifi BSSID"; - } - - try { - wifiIP = await _wifiInfo.getWifiIP(); - } on PlatformException catch (e) { - print(e.toString()); - wifiIP = "Failed to get Wifi IP"; - } - - setState(() { - _connectionStatus = '$result\n' - 'Wifi Name: $wifiName\n' - 'Wifi BSSID: $wifiBSSID\n' - 'Wifi IP: $wifiIP\n'; - }); - break; - case ConnectivityResult.mobile: - case ConnectivityResult.none: - setState(() => _connectionStatus = result.toString()); - break; - default: - setState(() => _connectionStatus = 'Failed to get connectivity.'); - break; - } - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml deleted file mode 100644 index 6303907c32d6..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: wifi_info_flutter_example -description: Demonstrates how to use the wifi_info_flutter plugin. -publish_to: none - -environment: - sdk: ">=2.12.0 <3.0.0" - -dependencies: - connectivity: ^3.0.0 - flutter: - sdk: flutter - wifi_info_flutter: - # When depending on this package from a real application you should use: - # wifi_info_flutter: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - -dev_dependencies: - integration_test: - sdk: flutter - flutter_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep b/packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h deleted file mode 100644 index 359562b7761c..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h +++ /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. - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@class FLTWifiInfoLocationDelegate; - -typedef void (^FLTWifiInfoLocationCompletion)(CLAuthorizationStatus); - -@interface FLTWifiInfoLocationHandler : NSObject - -+ (CLAuthorizationStatus)locationAuthorizationStatus; - -- (void)requestLocationAuthorization:(BOOL)always - completion:(_Nonnull FLTWifiInfoLocationCompletion)completionHnadler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m deleted file mode 100644 index 2fe19c5e70e9..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTWifiInfoLocationHandler.h" - -@interface FLTWifiInfoLocationHandler () - -@property(copy, nonatomic) FLTWifiInfoLocationCompletion completion; -@property(strong, nonatomic) CLLocationManager *locationManager; - -@end - -@implementation FLTWifiInfoLocationHandler - -+ (CLAuthorizationStatus)locationAuthorizationStatus { - return CLLocationManager.authorizationStatus; -} - -- (void)requestLocationAuthorization:(BOOL)always - completion:(FLTWifiInfoLocationCompletion)completionHandler { - CLAuthorizationStatus status = CLLocationManager.authorizationStatus; - if (status != kCLAuthorizationStatusAuthorizedWhenInUse && always) { - completionHandler(kCLAuthorizationStatusDenied); - return; - } else if (status != kCLAuthorizationStatusNotDetermined) { - completionHandler(status); - return; - } - - if (self.completion) { - // If a request is still in process, immediately return. - completionHandler(kCLAuthorizationStatusNotDetermined); - return; - } - - self.completion = completionHandler; - self.locationManager = [CLLocationManager new]; - self.locationManager.delegate = self; - if (always) { - [self.locationManager requestAlwaysAuthorization]; - } else { - [self.locationManager requestWhenInUseAuthorization]; - } -} - -- (void)locationManager:(CLLocationManager *)manager - didChangeAuthorizationStatus:(CLAuthorizationStatus)status { - if (status == kCLAuthorizationStatusNotDetermined) { - return; - } - if (self.completion) { - self.completion(status); - self.completion = nil; - } -} - -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h deleted file mode 100644 index 41f165717809..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h +++ /dev/null @@ -1,8 +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 - -@interface WifiInfoFlutterPlugin : NSObject -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m deleted file mode 100644 index 47bd90c4429b..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m +++ /dev/null @@ -1,134 +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 "WifiInfoFlutterPlugin.h" - -#import -#import "FLTWifiInfoLocationHandler.h" -#import "SystemConfiguration/CaptiveNetwork.h" - -#include - -#include - -@interface WifiInfoFlutterPlugin () -@property(strong, nonatomic) FLTWifiInfoLocationHandler* locationHandler; -@end - -@implementation WifiInfoFlutterPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - WifiInfoFlutterPlugin* instance = [[WifiInfoFlutterPlugin alloc] init]; - - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/wifi_info_flutter" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (NSString*)findNetworkInfo:(NSString*)key { - NSString* info = nil; - NSArray* interfaceNames = (__bridge_transfer id)CNCopySupportedInterfaces(); - for (NSString* interfaceName in interfaceNames) { - NSDictionary* networkInfo = - (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)interfaceName); - if (networkInfo[key]) { - info = networkInfo[key]; - } - } - return info; -} - -- (NSString*)getWifiName { - return [self findNetworkInfo:@"SSID"]; -} - -- (NSString*)getBSSID { - return [self findNetworkInfo:@"BSSID"]; -} - -- (NSString*)getWifiIP { - NSString* address = @"error"; - struct ifaddrs* interfaces = NULL; - struct ifaddrs* temp_addr = NULL; - int success = 0; - - // retrieve the current interfaces - returns 0 on success - success = getifaddrs(&interfaces); - if (success == 0) { - // Loop through linked list of interfaces - temp_addr = interfaces; - while (temp_addr != NULL) { - if (temp_addr->ifa_addr->sa_family == AF_INET) { - // Check if interface is en0 which is the wifi connection on the iPhone - if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { - // Get NSString from C String - address = [NSString - stringWithUTF8String:inet_ntoa(((struct sockaddr_in*)temp_addr->ifa_addr)->sin_addr)]; - } - } - - temp_addr = temp_addr->ifa_next; - } - } - - // Free memory - freeifaddrs(interfaces); - - return address; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([call.method isEqualToString:@"wifiName"]) { - result([self getWifiName]); - } else if ([call.method isEqualToString:@"wifiBSSID"]) { - result([self getBSSID]); - } else if ([call.method isEqualToString:@"wifiIPAddress"]) { - result([self getWifiIP]); - } else if ([call.method isEqualToString:@"getLocationServiceAuthorization"]) { - result([self convertCLAuthorizationStatusToString:[FLTWifiInfoLocationHandler - locationAuthorizationStatus]]); - } else if ([call.method isEqualToString:@"requestLocationServiceAuthorization"]) { - NSArray* arguments = call.arguments; - BOOL always = [arguments.firstObject boolValue]; - __weak typeof(self) weakSelf = self; - [self.locationHandler - requestLocationAuthorization:always - completion:^(CLAuthorizationStatus status) { - result([weakSelf convertCLAuthorizationStatusToString:status]); - }]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (NSString*)convertCLAuthorizationStatusToString:(CLAuthorizationStatus)status { - switch (status) { - case kCLAuthorizationStatusNotDetermined: { - return @"notDetermined"; - } - case kCLAuthorizationStatusRestricted: { - return @"restricted"; - } - case kCLAuthorizationStatusDenied: { - return @"denied"; - } - case kCLAuthorizationStatusAuthorizedAlways: { - return @"authorizedAlways"; - } - case kCLAuthorizationStatusAuthorizedWhenInUse: { - return @"authorizedWhenInUse"; - } - default: { - return @"unknown"; - } - } -} - -- (FLTWifiInfoLocationHandler*)locationHandler { - if (!_locationHandler) { - _locationHandler = [FLTWifiInfoLocationHandler new]; - } - return _locationHandler; -} -@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec b/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec deleted file mode 100644 index c3b3416ad767..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint wifi_info_flutter.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'wifi_info_flutter' - s.version = '0.0.1' - s.summary = 'A wifi information plugin for Flutter.' - s.description = <<-DESC -A Flutter plugin for retrieving wifi information from a device. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master' } - s.documentation_url = 'https://pub.dev' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } -end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart deleted file mode 100644 index 1c89ee82fc3e..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; - -// Export enums from the platform_interface so plugin users can use them directly. -export 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart' - show LocationAuthorizationStatus; - -/// Checks WI-FI status and more. -class WifiInfo { - WifiInfo._(); - - /// Constructs a singleton instance of [WifiInfo]. - /// - /// [WifiInfo] is designed to work as a singleton. - factory WifiInfo() => _singleton; - - static final WifiInfo _singleton = WifiInfo._(); - - static WifiInfoFlutterPlatform get _platform => - WifiInfoFlutterPlatform.instance; - - /// Obtains the wifi name (SSID) of the connected network - /// - /// Please note that it DOESN'T WORK on emulators (returns null). - /// - /// From android 8.0 onwards the GPS must be ON (high accuracy) - /// in order to be able to obtain the SSID. - Future getWifiName() { - return _platform.getWifiName(); - } - - /// Obtains the wifi BSSID of the connected network. - /// - /// Please note that it DOESN'T WORK on emulators (returns null). - /// - /// From Android 8.0 onwards the GPS must be ON (high accuracy) - /// in order to be able to obtain the BSSID. - Future getWifiBSSID() { - return _platform.getWifiBSSID(); - } - - /// Obtains the IP address of the connected wifi network - Future getWifiIP() { - return _platform.getWifiIP(); - } - - /// Request to authorize the location service (Only on iOS). - /// - /// This method will throw a [PlatformException] on Android. - /// - /// Returns a [LocationAuthorizationStatus] after user authorized or denied the location on this request. - /// - /// If the location information needs to be accessible all the time, set `requestAlwaysLocationUsage` to true. If user has - /// already granted a [LocationAuthorizationStatus.authorizedWhenInUse] prior to requesting an "always" access, it will return [LocationAuthorizationStatus.denied]. - /// - /// If the location service authorization is not determined prior to making this call, a platform standard UI of requesting a location service will pop up. - /// This UI will only show once unless the user re-install the app to their phone which resets the location service authorization to not determined. - /// - /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. - /// It can be replaced with other permission handling code/plugin if preferred. - /// To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: - /// * `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information - /// all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. - /// * `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is - /// running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. - /// - /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: - /// - /// * The app uses Core Location, and has the user’s authorization to use location information. - /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. - /// * The app has active VPN configurations installed. - /// - /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. - /// For example, - /// ```dart - /// if (Platform.isIOS) { - /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); - /// if (status == LocationAuthorizationStatus.notDetermined) { - /// status = await _connectivity.requestLocationServiceAuthorization(); - /// } - /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } else { - /// print('location service is not authorized, the data might not be correct'); - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// } else { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// ``` - /// - /// Ideally, a location service authorization should only be requested if the current authorization status is not determined. - /// - /// See also [getLocationServiceAuthorization] to obtain current location service status. - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) { - return _platform.requestLocationServiceAuthorization( - requestAlwaysLocationUsage: requestAlwaysLocationUsage, - ); - } - - /// Get the current location service authorization (Only on iOS). - /// - /// This method will throw a [PlatformException] on Android. - /// - /// Returns a [LocationAuthorizationStatus]. - /// If the returned value is [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] call - /// can request the authorization. - /// If the returned value is not [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] - /// will not initiate another request. It will instead return the "determined" status. - /// - /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. - /// It can be replaced with other permission handling code/plugin if preferred. - /// - /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: - /// - /// * The app uses Core Location, and has the user’s authorization to use location information. - /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. - /// * The app has active VPN configurations installed. - /// - /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. - /// For example, - /// ```dart - /// if (Platform.isIOS) { - /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); - /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } else { - /// print('location service is not authorized, the data might not be correct'); - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// } else { - /// wifiBSSID = await _connectivity.getWifiName(); - /// } - /// ``` - /// - /// See also [requestLocationServiceAuthorization] for requesting a location service authorization. - Future getLocationServiceAuthorization() { - return _platform.getLocationServiceAuthorization(); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml deleted file mode 100644 index cbda364aa35e..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: wifi_info_flutter -description: A new flutter plugin project. -repository: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+wifi_info_flutter%22 -version: 2.0.2 - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.wifi_info_flutter - pluginClass: WifiInfoFlutterPlugin - ios: - pluginClass: WifiInfoFlutterPlugin - -dependencies: - flutter: - sdk: flutter - wifi_info_flutter_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter diff --git a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart deleted file mode 100644 index 93cf378de437..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart +++ /dev/null @@ -1,84 +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:wifi_info_flutter/wifi_info_flutter.dart'; -import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; -import 'package:flutter_test/flutter_test.dart'; - -const String kWifiNameResult = '1337wifi'; -const String kWifiBSSIDResult = 'c0:ff:33:c0:d3:55'; -const String kWifiIpAddressResult = '127.0.0.1'; -const LocationAuthorizationStatus kRequestLocationResult = - LocationAuthorizationStatus.authorizedAlways; -const LocationAuthorizationStatus kGetLocationResult = - LocationAuthorizationStatus.authorizedAlways; - -void main() { - group('$WifiInfo', () { - late WifiInfo wifiInfo; - MockWifiInfoFlutterPlatform fakePlatform; - - setUp(() async { - fakePlatform = MockWifiInfoFlutterPlatform(); - WifiInfoFlutterPlatform.instance = fakePlatform; - wifiInfo = WifiInfo(); - }); - - test('getWifiName', () async { - String? result = await wifiInfo.getWifiName(); - expect(result, kWifiNameResult); - }); - - test('getWifiBSSID', () async { - String? result = await wifiInfo.getWifiBSSID(); - expect(result, kWifiBSSIDResult); - }); - - test('getWifiIP', () async { - String? result = await wifiInfo.getWifiIP(); - expect(result, kWifiIpAddressResult); - }); - - test('requestLocationServiceAuthorization', () async { - LocationAuthorizationStatus result = - await wifiInfo.requestLocationServiceAuthorization(); - expect(result, kRequestLocationResult); - }); - - test('getLocationServiceAuthorization', () async { - LocationAuthorizationStatus result = - await wifiInfo.getLocationServiceAuthorization(); - expect(result, kRequestLocationResult); - }); - }); -} - -class MockWifiInfoFlutterPlatform extends WifiInfoFlutterPlatform { - @override - Future getWifiName() async { - return kWifiNameResult; - } - - @override - Future getWifiBSSID() async { - return kWifiBSSIDResult; - } - - @override - Future getWifiIP() async { - return kWifiIpAddressResult; - } - - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) async { - return kRequestLocationResult; - } - - @override - Future getLocationServiceAuthorization() async { - return kGetLocationResult; - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.metadata b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.metadata deleted file mode 100644 index a6d65e2f3343..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: 4513e96a3022d70aa7686906c2e9bdfbbc448334 - channel: master - -project_type: package diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# 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/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md deleted file mode 100644 index 34f8e84cd780..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -## 2.0.1 - -* Update platform_plugin_interface version requirement. - -## 2.0.0 - -* Migrate to null safety. - -## 1.0.1 - -* Update Flutter SDK constraint. - -## 1.0.0 - -* Initial release of package. Includes support for retrieving wifi name, wifi BSSID, wifi ip address -and requesting location service authorization. diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md deleted file mode 100644 index f2039c3d5865..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# wifi_info_flutter_platform_interface - -A common platform interface for the [`wifi_info_flutter`][1] plugin. - -This interface allows platform-specific implementations of the `wifi_info_flutter` -plugin, as well as the plugin itself, to ensure they are supporting the -same interface. - -# Usage - -To implement a new platform-specific implementation of `wifi_info_flutter`, extend -[`WifiInfoFlutterPlatform`][2] with an implementation that performs the -platform-specific behavior, and when you register your plugin, set the default -`WifiInfoFlutterPlatform` by calling -`WifiInfoFlutterPlatform.instance = MyPlatformWifiInfoFlutter()`. - -# Note on breaking changes - -Strongly prefer non-breaking changes (such as adding a method to the interface) -over breaking changes for this package. - -See https://flutter.dev/go/platform-interface-breaking-changes for a discussion -on why a less-clean interface is preferable to a breaking change. - -[1]: ../ -[2]: lib/wifi_info_flutter_platform_interface.dart diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.dart deleted file mode 100644 index d5f05e6121a9..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.dart +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// The status of the location service authorization. -enum LocationAuthorizationStatus { - /// The authorization of the location service is not determined. - notDetermined, - - /// This app is not authorized to use location. - restricted, - - /// User explicitly denied the location service. - denied, - - /// User authorized the app to access the location at any time. - authorizedAlways, - - /// User authorized the app to access the location when the app is visible to them. - authorizedWhenInUse, - - /// Status unknown. - unknown -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart deleted file mode 100644 index 79f27e8cde44..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import '../wifi_info_flutter_platform_interface.dart'; - -/// An implementation of [WifiInfoFlutterPlatform] that uses method channels. -class MethodChannelWifiInfoFlutter extends WifiInfoFlutterPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - MethodChannel('plugins.flutter.io/wifi_info_flutter'); - - @override - Future getWifiName() async { - return methodChannel.invokeMethod('wifiName'); - } - - @override - Future getWifiBSSID() { - return methodChannel.invokeMethod('wifiBSSID'); - } - - @override - Future getWifiIP() { - return methodChannel.invokeMethod('wifiIPAddress'); - } - - @override - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) { - return methodChannel.invokeMethod( - 'requestLocationServiceAuthorization', [ - requestAlwaysLocationUsage - ]).then(_parseLocationAuthorizationStatus); - } - - @override - Future getLocationServiceAuthorization() { - return methodChannel - .invokeMethod('getLocationServiceAuthorization') - .then(_parseLocationAuthorizationStatus); - } -} - -/// Convert a String to a LocationAuthorizationStatus value. -LocationAuthorizationStatus _parseLocationAuthorizationStatus(String? result) { - return LocationAuthorizationStatus.values.firstWhere( - (LocationAuthorizationStatus status) => result == describeEnum(status), - orElse: () => LocationAuthorizationStatus.unknown, - ); -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart deleted file mode 100644 index 62330d4261a0..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'src/enums.dart'; -import 'src/method_channel_wifi_info_flutter.dart'; - -export 'src/enums.dart'; - -/// The interface that implementations of wifi_info_flutter must implement. -/// -/// Platform implementations should extend this class rather than implement it -/// as `wifi_info_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 -/// [ConnectivityPlatform] methods. -abstract class WifiInfoFlutterPlatform extends PlatformInterface { - /// Constructs a WifiInfoFlutterPlatform. - WifiInfoFlutterPlatform() : super(token: _token); - - static final Object _token = Object(); - - static WifiInfoFlutterPlatform _instance = MethodChannelWifiInfoFlutter(); - - /// The default instance of [WifiInfoFlutterPlatform] to use. - /// - /// Defaults to [MethodChannelWifiInfoFlutter]. - static WifiInfoFlutterPlatform get instance => _instance; - - /// Set the default instance of [WifiInfoFlutterPlatform] to use. - /// - /// Platform-specific plugins should set this with their own platform-specific - /// class that extends [WifiInfoFlutterPlatform] when they register - /// themselves. - static set instance(WifiInfoFlutterPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - /// Obtains the wifi name (SSID) of the connected network - Future getWifiName() { - throw UnimplementedError('getWifiName() has not been implemented.'); - } - - /// Obtains the wifi BSSID of the connected network. - Future getWifiBSSID() { - throw UnimplementedError('getWifiBSSID() has not been implemented.'); - } - - /// Obtains the IP address of the connected wifi network - Future getWifiIP() { - throw UnimplementedError('getWifiIP() has not been implemented.'); - } - - /// Request to authorize the location service (Only on iOS). - Future requestLocationServiceAuthorization( - {bool requestAlwaysLocationUsage = false}) { - throw UnimplementedError( - 'requestLocationServiceAuthorization() has not been implemented.', - ); - } - - /// Get the current location service authorization (Only on iOS). - Future getLocationServiceAuthorization() { - throw UnimplementedError( - 'getLocationServiceAuthorization() has not been implemented.', - ); - } -} diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml deleted file mode 100644 index 14ca643aa045..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: wifi_info_flutter_platform_interface -description: A common platform interface for the wifi_info_flutter plugin. -repository: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter_platform_interface -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+wifi_info_flutter%22 -version: 2.0.1 -# NOTE: We strongly prefer non-breaking changes, even at the expense of a -# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" - -dependencies: - plugin_platform_interface: ^2.0.0 - flutter: - sdk: flutter - -dev_dependencies: - pedantic: ^1.10.0 - flutter_test: - sdk: flutter diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart deleted file mode 100644 index 875f2ab4089a..000000000000 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart +++ /dev/null @@ -1,114 +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:wifi_info_flutter_platform_interface/src/enums.dart'; -import 'package:wifi_info_flutter_platform_interface/src/method_channel_wifi_info_flutter.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelWifiInfoFlutter', () { - final List log = []; - late MethodChannelWifiInfoFlutter methodChannelWifiInfoFlutter; - - setUp(() async { - methodChannelWifiInfoFlutter = MethodChannelWifiInfoFlutter(); - - methodChannelWifiInfoFlutter.methodChannel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'wifiName': - return '1337wifi'; - case 'wifiBSSID': - return 'c0:ff:33:c0:d3:55'; - case 'wifiIPAddress': - return '127.0.0.1'; - case 'requestLocationServiceAuthorization': - return 'authorizedAlways'; - case 'getLocationServiceAuthorization': - return 'authorizedAlways'; - default: - return null; - } - }); - log.clear(); - }); - - test('getWifiName', () async { - final String? result = await methodChannelWifiInfoFlutter.getWifiName(); - expect(result, '1337wifi'); - expect( - log, - [ - isMethodCall( - 'wifiName', - arguments: null, - ), - ], - ); - }); - - test('getWifiBSSID', () async { - final String? result = await methodChannelWifiInfoFlutter.getWifiBSSID(); - expect(result, 'c0:ff:33:c0:d3:55'); - expect( - log, - [ - isMethodCall( - 'wifiBSSID', - arguments: null, - ), - ], - ); - }); - - test('getWifiIP', () async { - final String? result = await methodChannelWifiInfoFlutter.getWifiIP(); - expect(result, '127.0.0.1'); - expect( - log, - [ - isMethodCall( - 'wifiIPAddress', - arguments: null, - ), - ], - ); - }); - - test('requestLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelWifiInfoFlutter - .requestLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'requestLocationServiceAuthorization', - arguments: [false], - ), - ], - ); - }); - - test('getLocationServiceAuthorization', () async { - final LocationAuthorizationStatus result = - await methodChannelWifiInfoFlutter.getLocationServiceAuthorization(); - expect(result, LocationAuthorizationStatus.authorizedAlways); - expect( - log, - [ - isMethodCall( - 'getLocationServiceAuthorization', - arguments: null, - ), - ], - ); - }); - }); -} diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 2b0f844de7e0..f735019d61c4 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -1,46 +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). -# -# DO NOT add new entries to the list, unless it is to push the legacy rules -# from a top-level package into more specific packages in order to incrementally -# migrate a federated plugin. +# 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 -- camera -- file_selector -- flutter_plugin_android_lifecycle -- google_maps_flutter -- google_sign_in -- image_picker -- in_app_purchase -- integration_test -- ios_platform_images -- local_auth -- plugin_platform_interface -- quick_actions -- shared_preferences -- url_launcher -- video_player -- webview_flutter - -# These plugins are deprecated in favor of the Community Plus versions, and -# will be removed from the repo once the critical support window has passed, -# so are not worth updating. -- android_alarm_manager -- android_intent -- battery -- connectivity -- device_info -- package_info -- sensors -- share -- wifi_info_flutter diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml index f6246aae2c86..8dd0fde5ef5f 100644 --- a/script/configs/exclude_all_plugins_app.yaml +++ b/script/configs/exclude_all_plugins_app.yaml @@ -8,9 +8,3 @@ # This is a permament entry, as it should never be a direct app dependency. - plugin_platform_interface -# TODO(mvanbeusekom): Remove the exclusion of the webview_flutter_android and -# webview_flutter_wkwebview packages once the native -# implementation is removed from the webview_flutter -# package (see https://github.com/flutter/flutter/issues/86286). -- webview_flutter_android -- webview_flutter_wkwebview diff --git a/script/configs/exclude_integration_android.yaml b/script/configs/exclude_integration_android.yaml index d8bd10b3a36e..653f3516727b 100644 --- a/script/configs/exclude_integration_android.yaml +++ b/script/configs/exclude_integration_android.yaml @@ -1,18 +1,2 @@ -# Currently missing harness files: https://github.com/flutter/flutter/issues/86749) -- camera/camera -- in_app_purchase/in_app_purchase -- in_app_purchase_android -- shared_preferences/shared_preferences -- url_launcher/url_launcher -- video_player/video_player - -# Deprecated; no plan to backfill the missing files -- android_intent -- connectivity/connectivity -- device_info/device_info -- sensors -- share -- wifi_info_flutter/wifi_info_flutter - # No integration tests to run: - espresso diff --git a/script/configs/exclude_integration_ios.yaml b/script/configs/exclude_integration_ios.yaml index e1ae6adf49cf..06283ae59c16 100644 --- a/script/configs/exclude_integration_ios.yaml +++ b/script/configs/exclude_integration_ios.yaml @@ -1,6 +1,4 @@ # Currently missing: https://github.com/flutter/flutter/issues/81695 -- in_app_purchase_ios +- in_app_purchase_storekit # Currently missing: https://github.com/flutter/flutter/issues/82208 - ios_platform_images -# Hangs on CI. Deprecated, so there is no plan to fix it. -- sensors 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 new file mode 100644 index 000000000000..09306691e5ed --- /dev/null +++ b/script/configs/exclude_integration_win32.yaml @@ -0,0 +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/exclude_native_ios.yaml b/script/configs/exclude_native_ios.yaml deleted file mode 100644 index 723fcfa64715..000000000000 --- a/script/configs/exclude_native_ios.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Deprecated; no plan to backfill the missing files -- battery -- connectivity/connectivity -- device_info/device_info -- package_info -- sensors -- wifi_info_flutter/wifi_info_flutter diff --git a/script/configs/exclude_native_macos.yaml b/script/configs/exclude_native_macos.yaml deleted file mode 100644 index 8a817a9c0178..000000000000 --- a/script/configs/exclude_native_macos.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Deprecated plugins that will not be getting unit test backfill. -- connectivity_macos -- package_info diff --git a/script/configs/exclude_native_unit_android.yaml b/script/configs/exclude_native_unit_android.yaml index 5ec80eee73a0..45197b94962f 100644 --- a/script/configs/exclude_native_unit_android.yaml +++ b/script/configs/exclude_native_unit_android.yaml @@ -1,11 +1,2 @@ -# Deprecated; no plan to backfill the missing files -- android_alarm_manager -- battery -- device_info/device_info -- package_info -- sensors -- share -- wifi_info_flutter/wifi_info_flutter - # No need for unit tests: - espresso diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml new file mode 100644 index 000000000000..9f14bf2315aa --- /dev/null +++ b/script/configs/temp_exclude_excerpt.yaml @@ -0,0 +1,24 @@ +# 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 +- video_player/video_player +- webview_flutter/webview_flutter +- webview_flutter_android +- webview_flutter_web diff --git a/script/install_chromium.sh b/script/install_chromium.sh new file mode 100755 index 000000000000..0d360fe98cfe --- /dev/null +++ b/script/install_chromium.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +set -e +set -x + +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/ +# +# 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" +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 "${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. +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 7e9cd3bec938..da14eae285e3 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,11 +1,124 @@ -## NEXT - +## 0.8.8 + +- Allows pre-release versions in `version-check`. + +## 0.8.7 + +- Supports empty custom analysis allow list files. +- `drive-examples` now validates files to ensure that they don't accidentally + use `test(...)`. +- Adds a new `dependabot-check` command to ensure complete Dependabot coverage. +- Adds `skip-if-not-supporting-dart-version` to allow for the same use cases + as `skip-if-not-supporting-flutter-version` but for packages without Flutter + constraints. + +## 0.8.6 + +- Adds `update-release-info` to apply changelog and optional version changes + across multiple packages. +- Fixes changelog validation when reverting to a `NEXT` state. +- Fixes multiplication of `--force` flag when publishing multiple packages. +- Adds minimum deployment target flags to `xcode-analyze` to allow + enforcing deprecation warning handling in advance of actually dropping + support for an OS version. +- Checks for template boilerplate in `readme-check`. +- `readme-check` now validates example READMEs when present. + +## 0.8.5 + +- Updates `test` to inculde the Dart unit tests of examples, if any. +- `drive-examples` now supports non-plugin packages. +- Commands that iterate over examples now include non-Flutter example packages. + +## 0.8.4 + +- `readme-check` now validates that there's a info tag on code blocks to + identify (and for supported languages, syntax highlight) the language. +- `readme-check` now has a `--require-excerpts` flag to require that any Dart + code blocks be managed by `code_excerpter`. + +## 0.8.3 + +- Adds a new `update-excerpts` command to maintain README files using the + `code-excerpter` package from flutter/site-shared. +- `license-check` now ignores submodules. +- Allows `make-deps-path-based` to skip packages it has alredy rewritten, so + that running multiple times won't fail after the first time. +- Removes UWP support, since Flutter has dropped support for UWP. + +## 0.8.2+1 + +- Adds a new `readme-check` command. +- Updates `publish-plugin` command documentation. +- Fixes `all-plugins-app` to preserve the original application's Dart SDK + version to avoid changing language feature opt-ins that the template may + rely on. +- Fixes `custom-test` to run `pub get` before running Dart test scripts. + +## 0.8.2 + +- Adds a new `custom-test` command. +- Switches from deprecated `flutter packages` alias to `flutter pub`. + +## 0.8.1 + +- Fixes an `analyze` regression in 0.8.0 with packages that have non-`example` + sub-packages. + +## 0.8.0 + +- Ensures that `firebase-test-lab` runs include an `integration_test` runner. +- Adds a `make-deps-path-based` command to convert inter-repo package + dependencies to path-based dependencies. +- Adds a (hidden) `--run-on-dirty-packages` flag for use with + `make-deps-path-based` in CI. +- `--packages` now allows using a federated plugin's package as a target without + fully specifying it (if it is not the same as the plugin's name). E.g., + `--packages=path_provide_ios` now works. +- `--run-on-changed-packages` now includes only the changed packages in a + federated plugin, not all packages in that plugin. +- Fixes `federation-safety-check` handling of plugin deletion, and of top-level + files in unfederated plugins whose names match federated plugin heuristics + (e.g., `packages/foo/foo_android.iml`). +- Adds an auto-retry for failed Firebase Test Lab tests as a short-term patch + for flake issues. +- Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar + `flutter` behavior. +- Validates `default_package` entries in plugins. +- Removes `allow-warnings` from the `podspecs` command. +- Adds `skip-if-not-supporting-flutter-version` to allow running tests using a + version of Flutter that not all packages support. (E.g., to allow for running + some tests against old versions of Flutter to help avoid accidental breakage.) + +## 0.7.3 + +- `native-test` now builds unit tests before running them on Windows and Linux, + matching the behavior of other platforms. +- Adds `--log-timing` to add timing information to package headers in looping + commands. +- Adds a `--check-for-missing-changes` flag to `version-check` that requires + version updates (except for recognized exemptions) and CHANGELOG changes when + modifying packages, unless the PR description explains why it's not needed. + +## 0.7.2 + +- Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). - `native-test --android`, `--ios`, and `--macos` now fail plugins that don't have unit tests, rather than skipping them. - Added a new `federation-safety-check` command to help catch changes to federated packages that have been done in such a way that they will pass in CI, but fail once the change is landed and published. - `publish-check` now validates that there is an `AUTHORS` file. +- Added flags to `version-check` to allow overriding the platform interface + major version change restriction. +- Improved error handling and error messages in CHANGELOG version checks. +- `license-check` now validates Kotlin files. +- `pubspec-check` now checks that the description is of the pub-recommended + length. +- Fix `license-check` when run on Windows with line ending conversion enabled. +- Fixed `pubspec-check` on Windows. +- Add support for `main` as a primary branch. `master` continues to work for + compatibility. ## 0.7.1 diff --git a/script/tool/README.md b/script/tool/README.md index 1a87f098757b..d3beb53a1103 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -51,8 +51,13 @@ following shows a number of common commands being run for a specific plugin. All examples assume running from source; see above for running the published version instead. -Note that the `plugins` argument, despite the name, applies to any package. -(It will likely be renamed `packages` in the future.) +Most commands take a `--packages` argument to control which package(s) the +command is targetting. An package name can be any of: +- The name of a package (e.g., `path_provider_android`). +- The name of a federated plugin (e.g., `path_provider`), in which case all + packages that make up that plugin will be targetted. +- A combination federated_plugin_name/package_name (e.g., + `path_provider/path_provider` for the app-facing package). ### Format Code @@ -79,10 +84,13 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name ```sh cd -dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --packages plugin_name -dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --packages plugin_name +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 ``` +Replace `--apk`/`--android` with the platform you want to test against +(omit it to get a list of valid options). + ### Run Native Tests `native-test` takes one or more platform flags to run tests for. By default it @@ -99,13 +107,56 @@ dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_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 plugin_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-plugin --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 @@ -119,10 +170,6 @@ _everything_, including untracked or uncommitted files in version control. 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. @@ -131,4 +178,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..8778b3de9d86 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. @@ -84,48 +81,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,6 +104,28 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { + // Analysis runs over the package and all subpackages, 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, + ...await getSubpackages(package).toList(), + ]; + for (final RepositoryPackage packageToGet in packagesToGet) { + if (packageToGet.directory.basename != 'example' || + !RepositoryPackage(packageToGet.directory.parent) + .pubspecFile + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', 'get'], + workingDir: packageToGet.directory); + if (exitCode != 0) { + return PackageResult.fail(['Unable to get dependencies']); + } + } + } + if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 82ed074c462a..1aade3575559 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -33,12 +33,11 @@ const int _exitInvalidPluginToolsConfig = 4; // Flutter build types. These are the values passed to `flutter build `. const String _flutterBuildTypeAndroid = 'apk'; -const String _flutterBuildTypeIos = 'ios'; +const String _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 { @@ -48,12 +47,11 @@ class BuildExamplesCommand extends PackageLoopingCommand { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(kPlatformLinux); - argParser.addFlag(kPlatformMacos); - argParser.addFlag(kPlatformWeb); - argParser.addFlag(kPlatformWindows); - argParser.addFlag(kPlatformWinUwp); - argParser.addFlag(kPlatformIos); + argParser.addFlag(platformLinux); + argParser.addFlag(platformMacOS); + argParser.addFlag(platformWeb); + argParser.addFlag(platformWindows); + argParser.addFlag(platformIOS); argParser.addFlag(_platformFlagApk); argParser.addOption( kEnableExperiment, @@ -68,41 +66,34 @@ class BuildExamplesCommand extends PackageLoopingCommand { { _platformFlagApk: const _PlatformDetails( 'Android', - pluginPlatform: kPlatformAndroid, + pluginPlatform: platformAndroid, flutterBuildType: _flutterBuildTypeAndroid, ), - kPlatformIos: const _PlatformDetails( + platformIOS: const _PlatformDetails( 'iOS', - pluginPlatform: kPlatformIos, - flutterBuildType: _flutterBuildTypeIos, + pluginPlatform: platformIOS, + flutterBuildType: _flutterBuildTypeIOS, extraBuildFlags: ['--no-codesign'], ), - kPlatformLinux: const _PlatformDetails( + platformLinux: const _PlatformDetails( 'Linux', - pluginPlatform: kPlatformLinux, + pluginPlatform: platformLinux, flutterBuildType: _flutterBuildTypeLinux, ), - kPlatformMacos: const _PlatformDetails( + platformMacOS: const _PlatformDetails( 'macOS', - pluginPlatform: kPlatformMacos, + pluginPlatform: platformMacOS, flutterBuildType: _flutterBuildTypeMacOS, ), - kPlatformWeb: const _PlatformDetails( + platformWeb: const _PlatformDetails( 'web', - pluginPlatform: kPlatformWeb, + pluginPlatform: platformWeb, flutterBuildType: _flutterBuildTypeWeb, ), - kPlatformWindows: const _PlatformDetails( - 'Win32', - pluginPlatform: kPlatformWindows, - pluginPlatformVariant: platformVariantWin32, - flutterBuildType: _flutterBuildTypeWin32, - ), - kPlatformWinUwp: const _PlatformDetails( - 'UWP', - pluginPlatform: kPlatformWindows, - pluginPlatformVariant: platformVariantWinUwp, - flutterBuildType: _flutterBuildTypeWinUwp, + platformWindows: const _PlatformDetails( + 'Windows', + pluginPlatform: platformWindows, + 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=$kPlatformWinUwp', '.'], - 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 new file mode 100644 index 000000000000..04ad880292b9 --- /dev/null +++ b/script/tool/lib/src/common/cmake.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:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; + +/// A utility class for interacting with CMake projects. +class CMakeProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + CMakeProject( + this.flutterProject, { + required this.buildMode, + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The build mode (e.g., Debug, Release). + /// + /// This is a constructor paramater because on Linux many properties depend + /// on the build mode since it uses a single-configuration generator. + final String buildMode; + + late final String _cmakeCommand = _determineCmakeCommand(); + + /// The project's platform directory name. + String get _platformDirName => platform.isWindows ? 'windows' : 'linux'; + + /// The project's 'example' build directory for this instance's platform. + Directory get buildDirectory { + Directory buildDir = + flutterProject.childDirectory('build').childDirectory(_platformDirName); + if (platform.isLinux) { + buildDir = buildDir + // TODO(stuartmorgan): Support arm64 if that ever becomes a supported + // CI configuration for the repository. + .childDirectory('x64') + // Linux uses a single-config generator, so the base build directory + // includes the configuration. + .childDirectory(buildMode.toLowerCase()); + } + return buildDir; + } + + File get _cacheFile => buildDirectory.childFile('CMakeCache.txt'); + + /// Returns the CMake command to run build commands for this project. + /// + /// Assumes the project has been built at least once, such that the CMake + /// generation step has run. + String getCmakeCommand() { + return _cmakeCommand; + } + + /// Returns the CMake command to run build commands for this project. This is + /// used to initialize _cmakeCommand, and should not be called directly. + /// + /// Assumes the project has been built at least once, such that the CMake + /// generation step has run. + String _determineCmakeCommand() { + // On Linux 'cmake' is expected to be in the path, so doesn't need to + // be lookup up and cached. + if (platform.isLinux) { + return 'cmake'; + } + final File cacheFile = _cacheFile; + String? command; + for (String line in cacheFile.readAsLinesSync()) { + line = line.trim(); + if (line.startsWith(_cacheCommandKey)) { + command = line.substring(line.indexOf('=') + 1).trim(); + break; + } + } + if (command == null) { + printError('Unable to find CMake command in ${cacheFile.path}'); + throw ToolExit(100); + } + return command; + } + + /// Whether or not the project is ready to have CMake commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => _cacheFile.existsSync(); + + /// Runs a `cmake` command with the given parameters. + Future runBuild( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + getCmakeCommand(), + [ + '--build', + buildDirectory.path, + '--target', + target, + if (platform.isWindows) ...['--config', buildMode], + ...arguments, + ], + ); + } +} diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index 53778eccb87f..b91029f1a5c8 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -4,72 +4,48 @@ 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. typedef Print = void Function(Object? object); /// Key for APK (Android) platform. -const String kPlatformAndroid = 'android'; +const String platformAndroid = 'android'; /// Key for IPA (iOS) platform. -const String kPlatformIos = 'ios'; +const String platformIOS = 'ios'; /// Key for linux platform. -const String kPlatformLinux = 'linux'; +const String platformLinux = 'linux'; /// Key for macos platform. -const String kPlatformMacos = 'macos'; +const String platformMacOS = 'macos'; /// Key for Web platform. -const String kPlatformWeb = 'web'; +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 kPlatformWindows = '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 kPlatformWinUwp = '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 [kPlatformWinUwp]. -const String platformVariantWinUwp = 'uwp'; +const String platformWindows = 'windows'; /// 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 1cdd2fcc409b..32d30e60feb5 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -31,10 +31,16 @@ class GitVersionFinder { } /// Get a list of all the changed files. - Future> getChangedFiles() async { + Future> getChangedFiles( + {bool includeUncommitted = false}) async { final String baseSha = await getBaseSha(); final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', baseSha, 'HEAD']); + .runCommand([ + 'diff', + '--name-only', + baseSha, + if (!includeUncommitted) 'HEAD' + ]); final String changedFilesStdout = changedFilesCommand.stdout.toString(); if (changedFilesStdout.isEmpty) { return []; @@ -75,8 +81,9 @@ class GitVersionFinder { io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], throwOnError: false); - if (baseShaFromMergeBase.stderr != null || - baseShaFromMergeBase.stdout == null) { + final String stdout = (baseShaFromMergeBase.stdout as String? ?? '').trim(); + final String stderr = (baseShaFromMergeBase.stdout 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 e7214bf29714..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'; @@ -14,9 +15,6 @@ const String _gradleWrapperNonWindows = 'gradlew'; class GradleProject { /// Creates an instance that runs commands for [project] with the given /// [processRunner]. - /// - /// If [log] is true, commands run by this instance will long various status - /// messages. GradleProject( this.flutterProject, { this.processRunner = const ProcessRunner(), @@ -24,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; @@ -33,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/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 973ac9995cb8..1a194bd45b9e 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 '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. @@ -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 @@ -170,7 +218,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// messages. DO NOT RELY on someone noticing a warning; instead, use it for /// things that might be useful to someone debugging an unexpected result. void logWarning(String warningMessage) { - print(Colorize(warningMessage)..yellow()); + _printColorized(warningMessage, Styles.YELLOW); if (_currentPackageEntry != null) { _packagesWithWarnings.add(_currentPackageEntry!); } else { @@ -219,6 +267,16 @@ 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(); final List targetPackages = @@ -227,8 +285,9 @@ abstract class PackageLoopingCommand extends PluginCommand { final Map results = {}; for (final PackageEnumerationEntry entry in targetPackages) { + final DateTime packageStart = DateTime.now(); _currentPackageEntry = entry; - _printPackageHeading(entry); + _printPackageHeading(entry, startTime: runStart); // Command implementations should never see excluded packages; they are // included at this level only for logging. @@ -239,18 +298,29 @@ 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()); result = PackageResult.fail(['Unhandled exception']); } if (result.state == RunState.skipped) { - final String message = - '${indentation}SKIPPING: ${result.details.first}'; - captureOutput ? print(message) : print(Colorize(message)..darkGray()); + _printColorized('${indentation}SKIPPING: ${result.details.first}', + Styles.DARK_GRAY); } results[entry] = result; + + // Only log an elapsed time for long output; for short output, comparing + // the relative timestamps of successive entries should be trivial. + if (shouldLogTiming && hasLongOutput) { + final Duration elapsedTime = DateTime.now().difference(packageStart); + _printColorized( + '\n[${entry.package.displayName} completed in ' + '${elapsedTime.inMinutes}m ${elapsedTime.inSeconds % 60}s]', + Styles.DARK_GRAY); + } } _currentPackageEntry = null; @@ -273,6 +343,36 @@ 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.toString()}'); + } + } + + 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.toString()}'); + } + } + + return await runForPackage(package); + } + void _printSuccess(String message) { captureOutput ? print(message) : printSuccess(message); } @@ -287,11 +387,20 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(PackageEnumerationEntry entry) { + void _printPackageHeading(PackageEnumerationEntry entry, + {required DateTime startTime}) { final String packageDisplayName = entry.package.displayName; String heading = entry.excluded ? 'Not running for $packageDisplayName; excluded' : 'Running for $packageDisplayName'; + + if (shouldLogTiming) { + final Duration relativeTime = DateTime.now().difference(startTime); + final String timeString = _formatDurationAsRelativeTime(relativeTime); + heading = + hasLongOutput ? '$heading [@$timeString]' : '[$timeString] $heading'; + } + if (hasLongOutput) { heading = ''' @@ -302,13 +411,7 @@ abstract class PackageLoopingCommand extends PluginCommand { } else if (!entry.excluded) { heading = '$heading...'; } - if (captureOutput) { - print(heading); - } else { - final Colorize colorizeHeading = Colorize(heading); - print( - entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); - } + _printColorized(heading, entry.excluded ? Styles.DARK_GRAY : Styles.CYAN); } /// Prints a summary of packges run, packages skipped, and warnings. @@ -401,4 +504,21 @@ abstract class PackageLoopingCommand extends PluginCommand { } _printError(failureListFooter); } + + /// Prints [message] in [color] unless [captureOutput] is set, in which case + /// it is printed without color. + void _printColorized(String message, Styles color) { + if (captureOutput) { + print(message); + } else { + print(Colorize(message)..apply(color)); + } + } + + /// Returns a duration [d] formatted as minutes:seconds. Does not use hours, + /// since time logging is primarily intended for CI, where durations should + /// always be less than an hour. + String _formatDurationAsRelativeTime(Duration d) { + return '${d.inMinutes}:${(d.inSeconds % 60).toString().padLeft(2, '0')}'; + } } 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..437bbf6df370 --- /dev/null +++ b/script/tool/lib/src/common/package_state_utils.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 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; + +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.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; +} + +/// 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 `getRelativePoixPath(package.directory, gitDir.path)` respectively; +/// they are arguments mainly to allow for caching the changed paths for an +/// entire command run. +PackageChangeState checkPackageChangeState( + RepositoryPackage package, { + required List changedPaths, + required String relativePackagePath, +}) { + final String packagePrefix = relativePackagePath.endsWith('/') + ? relativePackagePath + : '$relativePackagePath/'; + + bool hasChanges = false; + bool hasChangelogChange = false; + bool needsVersionChange = 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; + } + final bool isChangelog = components.first == 'CHANGELOG.md'; + if (isChangelog) { + hasChangelogChange = true; + } + + if (!needsVersionChange && + !isChangelog && + // One of a few special files example will be shown on pub.dev, but for + // anything else in the example publishing has no purpose. + !(components.first == 'example' && + !{'main.dart', 'readme.md', 'example.md'} + .contains(components.last.toLowerCase())) && + // Changes to tests don't need to be published. + !components.contains('test') && + !components.contains('androidTest') && + !components.contains('RunnerTests') && + !components.contains('RunnerUITests') && + // The top-level "tool" directory is for non-client-facing utility code, + // so doesn't need to be published. + components.first != 'tool' && + // Ignoring lints doesn't affect clients. + !components.contains('lint-baseline.xml')) { + needsVersionChange = true; + } + } + + return PackageChangeState( + hasChanges: hasChanges, + hasChangelogChange: hasChangelogChange, + needsVersionChange: needsVersionChange); +} diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 5d5cbd9abf6c..be9fb23e57a5 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -75,30 +75,43 @@ abstract class PluginCommand extends Command { help: 'Run the command on changed packages/plugins.\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' - 'The packages excluded with $_excludeArg is also excluded even if changed.\n' - 'See $_kBaseSha if a custom base is needed to determine the diff.\n\n' + 'Packages excluded with $_excludeArg are excluded even if changed.\n' + 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' 'Cannot be combined with $_packagesArg.\n'); + argParser.addFlag(_runOnDirtyPackagesArg, + help: + 'Run the command on packages with changes that have not been committed.\n' + 'Packages excluded with $_excludeArg are excluded even if changed.\n' + 'Cannot be combined with $_packagesArg.\n', + hide: true); argParser.addFlag(_packagesForBranchArg, help: 'This runs on all packages (equivalent to no package selection flag)\n' - 'on master, and behaves like --run-on-changed-packages on any other branch.\n\n' + 'on main (or master), and behaves like --run-on-changed-packages on ' + 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); - argParser.addOption(_kBaseSha, + argParser.addOption(_baseShaArg, help: 'The base sha used to determine git diff. \n' 'This is useful when $_runOnChangedPackagesArg is specified.\n' 'If not specified, merge-base is used as base sha.'); + argParser.addFlag(_logTimingArg, + help: 'Logs timing information.\n\n' + 'Currently only logs per-package timing for multi-package commands, ' + 'but more information may be added in the future.'); } - static const String _pluginsArg = 'plugins'; - static const String _packagesArg = 'packages'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; + static const String _baseShaArg = 'base-sha'; static const String _excludeArg = 'exclude'; - static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _logTimingArg = 'log-timing'; + static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; - static const String _kBaseSha = 'base-sha'; + static const String _pluginsArg = '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. final Directory packagesDir; @@ -179,9 +192,16 @@ 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 + /// analyzing their runtime (e.g., the per-package time for multi-package + /// commands). + bool get shouldLogTiming => getBoolArg(_logTimingArg); + void _checkSharding() { final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); @@ -278,6 +298,7 @@ abstract class PluginCommand extends Command { final Set packageSelectionFlags = { _packagesArg, _runOnChangedPackagesArg, + _runOnDirtyPackagesArg, _packagesForBranchArg, }; if (packageSelectionFlags @@ -289,7 +310,7 @@ abstract class PluginCommand extends Command { throw ToolExit(exitInvalidArguments); } - Set plugins = Set.from(getStringListArg(_packagesArg)); + Set packages = Set.from(getStringListArg(_packagesArg)); final bool runOnChangedPackages; if (getBoolArg(_runOnChangedPackagesArg)) { @@ -301,7 +322,7 @@ abstract class PluginCommand extends Command { 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { - runOnChangedPackages = branch != 'master'; + runOnChangedPackages = branch != 'master' && branch != 'main'; // Log the mode for auditing what was intended to run. print('--$_packagesForBranchArg: running on ' '${runOnChangedPackages ? 'changed' : 'all'} packages'); @@ -320,7 +341,21 @@ abstract class PluginCommand extends Command { final List changedFiles = await gitVersionFinder.getChangedFiles(); if (!_changesRequireFullTest(changedFiles)) { - plugins = _getChangedPackages(changedFiles); + packages = _getChangedPackageNames(changedFiles); + } + } else if (getBoolArg(_runOnDirtyPackagesArg)) { + final GitVersionFinder gitVersionFinder = + GitVersionFinder(await gitDir, 'HEAD'); + print('Running for all packages that have uncommitted changes\n'); + // _changesRequireFullTest is deliberately not used here, as this flag is + // intended for use in CI to re-test packages changed by + // 'make-deps-path-based'. + packages = _getChangedPackageNames( + await gitVersionFinder.getChangedFiles(includeUncommitted: true)); + // For the same reason, empty is not treated as "all packages" as it is + // for other flags. + if (packages.isEmpty) { + return; } } @@ -335,8 +370,8 @@ abstract class PluginCommand extends Command { await for (final FileSystemEntity entity in dir.list(followLinks: false)) { // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { - if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { + if (isPackage(entity)) { + if (packages.isEmpty || packages.contains(p.basename(entity.path))) { yield PackageEnumerationEntry( RepositoryPackage(entity as Directory), excluded: excludedPluginNames.contains(entity.basename)); @@ -345,22 +380,24 @@ abstract class PluginCommand extends Command { // Look for Dart packages under this top-level directory. await for (final FileSystemEntity subdir in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - path.relative(subdir.path, from: dir.path); - final String packageName = path.basename(subdir.path); - final String basenamePath = path.basename(entity.path); - if (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath)) { + 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) + // - group name (path_provider), which matches all packages in + // the group + final Set possibleMatches = { + path.basename(subdir.path), // package name + path.basename(entity.path), // group name + path.relative(subdir.path, from: dir.path), // fully specified + }; + if (packages.isEmpty || + packages.intersection(possibleMatches).isNotEmpty) { yield PackageEnumerationEntry( RepositoryPackage(subdir as Directory), - excluded: excludedPluginNames.contains(basenamePath) || - excludedPluginNames.contains(packageName) || - excludedPluginNames.contains(relativePath)); + excluded: excludedPluginNames + .intersection(possibleMatches) + .isNotEmpty); } } } @@ -374,21 +411,30 @@ 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 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* getSubpackages(plugin.package).map((RepositoryPackage package) => + PackageEnumerationEntry(package, excluded: plugin.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() { @@ -404,34 +450,59 @@ 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 `_kBaseSha` and [gitDir]. + /// Retrieve an instance of [GitVersionFinder] based on `_baseShaArg` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. Future retrieveVersionFinder() async { - final String baseSha = getStringArg(_kBaseSha); + final String baseSha = getStringArg(_baseShaArg); final GitVersionFinder gitVersionFinder = GitVersionFinder(await gitDir, baseSha); return gitVersionFinder; } - // Returns packages that have been changed given a list of changed files. + // Returns the names of packages that have been changed given a list of + // changed files. + // + // The names will either be the actual package names, or potentially + // group/name specifiers (for example, path_provider/path_provider) for + // packages in federated plugins. // // The paths must use POSIX separators (e.g., as provided by git output). - Set _getChangedPackages(List changedFiles) { + Set _getChangedPackageNames(List changedFiles) { final Set packages = {}; + + // A helper function that returns true if candidatePackageName looks like an + // implementation package of a plugin called pluginName. Used to determine + // if .../packages/parentName/candidatePackageName/... + // looks like a path in a federated plugin package (candidatePackageName) + // rather than a top-level package (parentName). + bool isFederatedPackage(String candidatePackageName, String parentName) { + return candidatePackageName == parentName || + candidatePackageName.startsWith('${parentName}_'); + } + for (final String path in changedFiles) { final List pathComponents = p.posix.split(path); final int packagesIndex = pathComponents.indexWhere((String element) => element == 'packages'); if (packagesIndex != -1) { - packages.add(pathComponents[packagesIndex + 1]); + // Find the name of the directory directly under packages. This is + // either the name of the package, or a plugin group directory for + // a federated plugin. + final String topLevelName = pathComponents[packagesIndex + 1]; + String packageName = topLevelName; + if (packagesIndex + 2 < pathComponents.length && + isFederatedPackage( + pathComponents[packagesIndex + 2], topLevelName)) { + // This looks like a federated package; use the full specifier if + // the name would be ambiguous (i.e., for the app-facing package). + packageName = pathComponents[packagesIndex + 2]; + if (packageName == topLevelName) { + packageName = '$topLevelName/$packageName'; + } + } + packages.add(packageName); } } if (packages.isEmpty) { diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 6cfe9928d689..f33d3d73bb75 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.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:file/file.dart'; import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; @@ -37,14 +36,13 @@ bool pluginSupportsPlatform( String platform, RepositoryPackage plugin, { PlatformSupport? requiredMode, - String? variant, }) { - assert(platform == kPlatformIos || - platform == kPlatformAndroid || - platform == kPlatformWeb || - platform == kPlatformMacos || - platform == kPlatformWindows || - platform == kPlatformLinux); + assert(platform == platformIOS || + platform == platformAndroid || + platform == platformWeb || + platform == platformMacOS || + platform == platformWindows || + platform == platformLinux); final YamlMap? platformEntry = _readPlatformPubspecSectionForPlugin(platform, plugin); @@ -61,33 +59,13 @@ 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 = { - kPlatformWindows: platformVariantWin32, - }; - if (variant != defaultVariants[platform]) { - return false; - } - } - } - return true; } /// Returns true if [plugin] includes native code for [platform], as opposed to /// being implemented entirely in Dart. bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { - if (platform == kPlatformWeb) { + if (platform == platformWeb) { // Web plugins are always Dart-only. return false; } @@ -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 feece7c1cdff..5f448d36d7e2 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -4,9 +4,13 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; +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 @@ -47,19 +51,94 @@ 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()); + + /// Returns the parsed [pubspecFile]. + /// + /// 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 => + directory.basename.endsWith('_platform_interface'); + + /// True if this appears to be a platform implementation package, according to + /// repository conventions. + bool get isPlatformImplementation => + // Any part of a federated plugin that isn't the platform interface and + // isn't the app-facing package should be an implementation package. + isFederated && + !isPlatformInterface && + directory.basename != directory.parent.basename; + /// Returns the Flutter example packages contained in the package, if any. Iterable getExamples() { final Directory exampleDirectory = directory.childDirectory('example'); 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 @@ -67,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 6dbebf2f5c74..595779b8be68 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -30,11 +30,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.'; @@ -73,7 +76,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { '--template=app', '--project-name=all_plugins', '--android-language=java', - appDirectory.path, + _appDirectory.path, ], ); @@ -83,8 +86,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()) { @@ -93,10 +96,13 @@ class CreateAllPluginsAppCommand extends PluginCommand { final StringBuffer newGradle = StringBuffer(); for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion 16')) { - // Android SDK 20 is required by Google maps. - // Android SDK 19 is required by WebView. + if (line.contains('minSdkVersion')) { + // minSdkVersion 20 is required by Google maps. + // minSdkVersion 19 is required by WebView. newGradle.writeln('minSdkVersion 20'); + } else if (line.contains('compileSdkVersion')) { + // compileSdkVersion 31 is required by Camera. + newGradle.writeln('compileSdkVersion 31'); } else { newGradle.writeln(line); } @@ -104,7 +110,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( @@ -116,8 +122,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') @@ -144,6 +150,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( @@ -151,9 +169,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'), @@ -163,8 +179,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 { @@ -175,8 +190,7 @@ class CreateAllPluginsAppCommand extends PluginCommand { final RepositoryPackage package = entry.package; final Directory pluginDirectory = package.directory; final String pluginName = pluginDirectory.basename; - final File pubspecFile = package.pubspecFile; - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = package.parsePubspec(); if (pubspec.publishTo != 'none') { pathDependencies[pluginName] = PathDependency(pluginDirectory.path); @@ -210,7 +224,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}'); 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 b3434b0659f3..45e20c0f13cf 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -25,21 +25,18 @@ class DriveExamplesCommand extends PackageLoopingCommand { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(kPlatformAndroid, + argParser.addFlag(platformAndroid, help: 'Runs the Android implementation of the examples'); - argParser.addFlag(kPlatformIos, + argParser.addFlag(platformIOS, help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(kPlatformLinux, + argParser.addFlag(platformLinux, help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(kPlatformMacos, + argParser.addFlag(platformMacOS, help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(kPlatformWeb, + argParser.addFlag(platformWeb, help: 'Runs the web implementation of the examples'); - argParser.addFlag(kPlatformWindows, - help: 'Runs the Windows (Win32) implementation of the examples'); - argParser.addFlag(kPlatformWinUwp, - help: - 'Runs the UWP implementation of the examples [currently a no-op]'); + argParser.addFlag(platformWindows, + help: 'Runs the Windows implementation of the examples'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -64,13 +61,12 @@ class DriveExamplesCommand extends PackageLoopingCommand { @override Future initializeRun() async { final List platformSwitches = [ - kPlatformAndroid, - kPlatformIos, - kPlatformLinux, - kPlatformMacos, - kPlatformWeb, - kPlatformWindows, - kPlatformWinUwp, + platformAndroid, + platformIOS, + platformLinux, + platformMacOS, + platformWeb, + platformWindows, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -85,12 +81,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } - if (getBoolArg(kPlatformWinUwp)) { - logWarning('Driving UWP applications is not yet supported'); - } - String? androidDevice; - if (getBoolArg(kPlatformAndroid)) { + if (getBoolArg(platformAndroid)) { final List devices = await _getDevicesForPlatform('android'); if (devices.isEmpty) { printError('No Android devices available'); @@ -99,74 +91,64 @@ class DriveExamplesCommand extends PackageLoopingCommand { androidDevice = devices.first; } - String? iosDevice; - if (getBoolArg(kPlatformIos)) { + String? iOSDevice; + if (getBoolArg(platformIOS)) { final List devices = await _getDevicesForPlatform('ios'); if (devices.isEmpty) { printError('No iOS devices available'); throw ToolExit(_exitNoAvailableDevice); } - iosDevice = devices.first; + iOSDevice = devices.first; } _targetDeviceFlags = >{ - if (getBoolArg(kPlatformAndroid)) - kPlatformAndroid: ['-d', androidDevice!], - if (getBoolArg(kPlatformIos)) kPlatformIos: ['-d', iosDevice!], - if (getBoolArg(kPlatformLinux)) kPlatformLinux: ['-d', 'linux'], - if (getBoolArg(kPlatformMacos)) kPlatformMacos: ['-d', 'macos'], - if (getBoolArg(kPlatformWeb)) - kPlatformWeb: [ + if (getBoolArg(platformAndroid)) + platformAndroid: ['-d', androidDevice!], + if (getBoolArg(platformIOS)) platformIOS: ['-d', iOSDevice!], + if (getBoolArg(platformLinux)) platformLinux: ['-d', 'linux'], + if (getBoolArg(platformMacOS)) platformMacOS: ['-d', 'macos'], + if (getBoolArg(platformWeb)) + platformWeb: [ '-d', 'web-server', '--web-port=7357', - '--browser-name=chrome' + '--browser-name=chrome', + if (platform.environment.containsKey('CHROME_EXECUTABLE')) + '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', ], - if (getBoolArg(kPlatformWindows)) - kPlatformWindows: ['-d', 'windows'], - // TODO(stuartmorgan): Check these flags once drive supports UWP: - // https://github.com/flutter/flutter/issues/82821 - if (getBoolArg(kPlatformWinUwp)) - kPlatformWinUwp: ['-d', 'winuwp'], + if (getBoolArg(platformWindows)) + platformWindows: ['-d', 'windows'], }; } @override Future runForPackage(RepositoryPackage package) async { - if (package.directory.basename.endsWith('_platform_interface') && - !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 == kPlatformWindows) { - variant = platformVariantWin32; - } else if (platform == kPlatformWinUwp) { - 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()) { @@ -174,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'); @@ -191,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) { @@ -214,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 = []; @@ -292,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 fd53d6cbaa67..383637a9e896 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -8,7 +8,6 @@ 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'; @@ -81,7 +80,8 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { // Count the top-level plugin as changed. _changedPlugins.add(packageName); if (relativeComponents[0] == packageName || - relativeComponents[0].startsWith('${packageName}_')) { + (relativeComponents.length > 1 && + relativeComponents[0].startsWith('${packageName}_'))) { packageName = relativeComponents.removeAt(0); } @@ -109,7 +109,7 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { return PackageResult.skip('Not a federated plugin.'); } - if (package.directory.basename.endsWith('_platform_interface')) { + if (package.isPlatformInterface) { // As the leaf nodes in the graph, a published package interface change is // assumed to be correct, and other changes are validated against that. return PackageResult.skip( @@ -178,6 +178,10 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { String pubspecRepoRelativePosixPath) async { final File pubspecFile = childFileWithSubcomponents( packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); + if (!pubspecFile.existsSync()) { + // If the package was deleted, nothing will be published. + return false; + } final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo == 'none') { return false; diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 941cba3a6945..4505259b731f 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -54,7 +54,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { splitCommas: false, defaultsTo: [ 'model=walleye,version=26', - 'model=flame,version=29' + 'model=redfin,version=30' ], help: 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); @@ -119,26 +119,59 @@ 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.'); } - if (!androidDirectory + final Directory uiTestDirectory = androidDirectory .childDirectory('app') .childDirectory('src') - .childDirectory('androidTest') - .existsSync()) { + .childDirectory('androidTest'); + if (!uiTestDirectory.existsSync()) { printError('No androidTest directory found.'); return PackageResult.fail( ['No tests ran (use --exclude if this is intentional).']); } + // Ensure that the Dart integration tests will be run, not just native UI + // tests. + if (!await _testsContainDartIntegrationTestRunner(uiTestDirectory)) { + printError('No integration_test runner found. ' + 'See the integration_test package README for setup instructions.'); + return PackageResult.fail(['No integration_test runner.']); + } + // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(example.directory, + final GradleProject project = GradleProject(example, processRunner: processRunner, platform: platform); if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); @@ -155,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...'); @@ -167,31 +200,24 @@ 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++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '7m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - ]; - for (final String device in getStringListArg('device')) { - args.addAll(['--device', device]); - } - final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: example.directory); + 'plugins_android_test/${package.displayName}/$buildId/$testRunId/' + '${example.directory.basename}/${resultsCounter++}/'; - if (exitCode != 0) { - printError('Test failure for $testName'); + // Automatically retry failures; there is significant flake with these + // tests whose cause isn't yet understood, and having to re-run the + // entire shard for a flake in any one test is extremely slow. This should + // be removed once the root cause of the flake is understood. + // See https://github.com/flutter/flutter/issues/95063 + const int maxRetries = 2; + bool passing = false; + for (int i = 1; i <= maxRetries && !passing; ++i) { + if (i > 1) { + logWarning('$testName failed on attempt ${i - 1}. Retrying...'); + } + passing = await _runFirebaseTest(example, test, resultsDir: resultsDir); + } + if (!passing) { + printError('Test failure for $testName after $maxRetries attempts'); errors.add('$testName failed tests'); } } @@ -230,6 +256,42 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } + /// Runs [test] from [example] as a Firebase Test Lab test, returning true if + /// the test passed. + /// + /// [resultsDir] should be a unique-to-the-test-run directory to store the + /// results on the server. + Future _runFirebaseTest( + RepositoryPackage example, + File test, { + required String resultsDir, + }) async { + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '7m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + for (final String device in getStringListArg('device')) ...[ + '--device', + device + ], + ]; + final int exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: example.directory); + + return exitCode == 0; + } + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is /// already configured. /// @@ -263,12 +325,10 @@ 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; @@ -280,4 +340,19 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { file is File && file.basename.endsWith('_test.dart')) .cast(); } + + /// Returns true if any of the test files in [uiTestDirectory] contain the + /// annotation that means that the test will reports the results of running + /// the Dart integration tests. + Future _testsContainDartIntegrationTestRunner( + Directory uiTestDirectory) async { + return uiTestDirectory + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast() + .any((File file) { + return file.basename.endsWith('.java') && + file.readAsStringSync().contains('@RunWith(FlutterTestRunner.class)'); + }); + } } diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index f24a99436c87..f640cbaa5f6c 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -130,15 +130,14 @@ 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); } print('Formatting .cc, .cpp, .h, .m, and .mm files...'); final int exitCode = await _runBatched( - getStringArg('clang-format'), ['-i', '--style=Google'], + getStringArg('clang-format'), ['-i', '--style=file'], files: clangFiles); if (exitCode != 0) { printError( @@ -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 8cee46b45a4c..5e74d846c13f 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -3,7 +3,9 @@ // 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'; @@ -16,6 +18,7 @@ const Set _codeFileExtensions = { '.h', '.html', '.java', + '.kt', '.m', '.mm', '.swift', @@ -104,7 +107,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /// Validates that code files have copyright and license blocks. class LicenseCheckCommand extends PluginCommand { /// 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'; @@ -115,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)) && @@ -205,7 +217,10 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in codeFiles) { print('Checking ${file.path}'); - final String content = await file.readAsString(); + // On Windows, git may auto-convert line endings on checkout; this should + // still pass since they will be converted back on commit. + final String content = + (await file.readAsString()).replaceAll('\r\n', '\n'); final String firstParyLicense = firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? @@ -226,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); @@ -243,7 +257,10 @@ class LicenseCheckCommand extends PluginCommand { for (final File file in files) { print('Checking ${file.path}'); - if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + // On Windows, git may auto-convert line endings on checkout; this should + // still pass since they will be converted back on commit. + final String contents = file.readAsStringSync().replaceAll('\r\n', '\n'); + if (!contents.contains(_fullBsdLicenseText)) { incorrectLicenseFiles.add(file); } } @@ -268,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 a7b5c4f2e8bf..8ba1d643a89b 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -12,9 +12,9 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -/// Lint the CocoaPod podspecs and run unit tests. +/// Run 'gradlew lint'. /// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +/// See https://developer.android.com/studio/write/lint. class LintAndroidCommand extends PackageLoopingCommand { /// Creates an instance of the linter command. LintAndroidCommand( @@ -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(kPlatformAndroid, package, + if (!pluginSupportsPlatform(platformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( 'Plugin does not have an Android implemenatation.'); } - 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/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index ee44a82da5b9..198dd9472115 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -26,13 +26,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addMultiOption('ignore-warnings', - help: - 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs ' - 'with this basename (example: plugins with known warnings)', - valueHelp: 'podspec_file_name'); - } + }) : super(packagesDir, processRunner: processRunner, platform: platform); @override final String name = 'podspecs'; @@ -118,8 +112,6 @@ class LintPodspecsCommand extends PackageLoopingCommand { Future _runPodLint(String podspecPath, {required bool libraryLint}) async { - final bool allowWarnings = (getStringListArg('ignore-warnings')) - .contains(p.basenameWithoutExtension(podspecPath)); final List arguments = [ 'lib', 'lint', @@ -127,7 +119,6 @@ class LintPodspecsCommand extends PackageLoopingCommand { '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. '--skip-tests', '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. - if (allowWarnings) '--allow-warnings', if (libraryLint) '--use-libraries' ]; diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 70a6ab516037..966e7b6be56a 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -7,11 +7,13 @@ 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/dependabot_check_command.dart'; 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 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; import 'firebase_test_lab_command.dart'; @@ -20,11 +22,15 @@ import 'license_check_command.dart'; import 'lint_android_command.dart'; import 'lint_podspecs_command.dart'; 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 'pubspec_check_command.dart'; +import 'readme_check_command.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'; @@ -49,6 +55,8 @@ 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)) @@ -58,10 +66,14 @@ void main(List args) { ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) ..addCommand(NativeTestCommand(packagesDir)) + ..addCommand(MakeDepsPathBasedCommand(packagesDir)) ..addCommand(PublishCheckCommand(packagesDir)) ..addCommand(PublishPluginCommand(packagesDir)) ..addCommand(PubspecCheckCommand(packagesDir)) + ..addCommand(ReadmeCheckCommand(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 new file mode 100644 index 000000000000..4bbecb4d2244 --- /dev/null +++ b/script/tool/lib/src/make_deps_path_based_command.dart @@ -0,0 +1,275 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; + +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; + +const int _exitPackageNotFound = 3; +const int _exitCannotUpdatePubspec = 4; + +enum _RewriteOutcome { changed, noChangesNeeded, alreadyChanged } + +/// Converts all dependencies on target packages to path-based dependencies. +/// +/// This is to allow for pre-publish testing of changes that could affect other +/// packages in the repository. For instance, this allows for catching cases +/// where a non-breaking change to a platform interface package of a federated +/// plugin would cause post-publish analyzer failures in another package of that +/// plugin. +class MakeDepsPathBasedCommand extends PluginCommand { + /// Creates an instance of the command to convert selected dependencies to + /// path-based. + MakeDepsPathBasedCommand( + Directory packagesDir, { + GitDir? gitDir, + }) : super(packagesDir, gitDir: gitDir) { + argParser.addMultiOption(_targetDependenciesArg, + help: + 'The names of the packages to convert to path-based dependencies.\n' + 'Ignored if --$_targetDependenciesWithNonBreakingUpdatesArg is ' + 'passed.', + valueHelp: 'some_package'); + argParser.addFlag( + _targetDependenciesWithNonBreakingUpdatesArg, + help: 'Causes all packages that have non-breaking version changes ' + 'when compared against the git base to be treated as target ' + 'packages.', + ); + } + + static const String _targetDependenciesArg = 'target-dependencies'; + static const String _targetDependenciesWithNonBreakingUpdatesArg = + 'target-dependencies-with-non-breaking-updates'; + + // The comment to add to temporary dependency overrides. + static const String _dependencyOverrideWarningComment = + '# FOR TESTING ONLY. DO NOT MERGE.'; + + @override + final String name = 'make-deps-path-based'; + + @override + final String description = + 'Converts package dependencies to path-based references.'; + + @override + Future run() async { + final Set targetDependencies = + getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg) + ? await _getNonBreakingUpdatePackages() + : getStringListArg(_targetDependenciesArg).toSet(); + + if (targetDependencies.isEmpty) { + print('No target dependencies; nothing to do.'); + return; + } + print('Rewriting references to: ${targetDependencies.join(', ')}...'); + + final Map localDependencyPackages = + _findLocalPackages(targetDependencies); + + final String repoRootPath = (await gitDir).path; + for (final File pubspec in await _getAllPubspecs()) { + final String displayPath = p.posix.joinAll( + path.split(path.relative(pubspec.absolute.path, from: repoRootPath))); + final _RewriteOutcome outcome = await _addDependencyOverridesIfNecessary( + pubspec, localDependencyPackages); + switch (outcome) { + case _RewriteOutcome.changed: + print(' Modified $displayPath'); + break; + case _RewriteOutcome.alreadyChanged: + print(' Skipped $displayPath - Already rewritten'); + break; + case _RewriteOutcome.noChangesNeeded: + break; + } + } + } + + Map _findLocalPackages(Set packageNames) { + final Map targets = + {}; + for (final String packageName in packageNames) { + final Directory topLevelCandidate = + packagesDir.childDirectory(packageName); + // If packages// exists, then either that directory is the + // package, or packages/// exists and is the + // package (in the case of a federated plugin). + if (topLevelCandidate.existsSync()) { + final Directory appFacingCandidate = + topLevelCandidate.childDirectory(packageName); + targets[packageName] = RepositoryPackage(appFacingCandidate.existsSync() + ? appFacingCandidate + : topLevelCandidate); + continue; + } + // If there is no packages/ directory, then either the + // packages doesn't exist, or it is a sub-package of a federated plugin. + // If it's the latter, it will be a directory whose name is a prefix. + for (final FileSystemEntity entity in packagesDir.listSync()) { + if (entity is Directory && packageName.startsWith(entity.basename)) { + final Directory subPackageCandidate = + entity.childDirectory(packageName); + if (subPackageCandidate.existsSync()) { + targets[packageName] = RepositoryPackage(subPackageCandidate); + break; + } + } + } + + if (!targets.containsKey(packageName)) { + printError('Unable to find package "$packageName"'); + throw ToolExit(_exitPackageNotFound); + } + } + return targets; + } + + /// If [pubspecFile] has any dependencies on packages in [localDependencies], + /// adds dependency_overrides entries to redirect them to the local version + /// using path-based dependencies. + Future<_RewriteOutcome> _addDependencyOverridesIfNecessary(File pubspecFile, + Map localDependencies) async { + final String pubspecContents = pubspecFile.readAsStringSync(); + final Pubspec pubspec = Pubspec.parse(pubspecContents); + // Fail if there are any dependency overrides already, other than ones + // created by this script. If support for that is needed at some point, it + // can be added, but currently it's not and relying on that makes the logic + // here much simpler. + if (pubspec.dependencyOverrides.isNotEmpty) { + if (pubspecContents.contains(_dependencyOverrideWarningComment)) { + return _RewriteOutcome.alreadyChanged; + } + printError( + 'Plugins with dependency overrides are not currently supported.'); + throw ToolExit(_exitCannotUpdatePubspec); + } + + final Iterable packagesToOverride = pubspec.dependencies.keys.where( + (String packageName) => localDependencies.containsKey(packageName)); + if (packagesToOverride.isNotEmpty) { + final String commonBasePath = packagesDir.path; + // Find the relative path to the common base. + final int packageDepth = path + .split(path.relative(pubspecFile.parent.absolute.path, + from: commonBasePath)) + .length; + final List relativeBasePathComponents = + List.filled(packageDepth, '..'); + // This is done via strings rather than by manipulating the Pubspec and + // then re-serialiazing so that it's a localized change, rather than + // rewriting the whole file (e.g., destroying comments), which could be + // more disruptive for local use. + String newPubspecContents = ''' +$pubspecContents + +$_dependencyOverrideWarningComment +dependency_overrides: +'''; + for (final String packageName in packagesToOverride) { + // Find the relative path from the common base to the local package. + final List repoRelativePathComponents = path.split( + path.relative(localDependencies[packageName]!.path, + from: commonBasePath)); + newPubspecContents += ''' + $packageName: + path: ${p.posix.joinAll([ + ...relativeBasePathComponents, + ...repoRelativePathComponents, + ])} +'''; + } + pubspecFile.writeAsStringSync(newPubspecContents); + return _RewriteOutcome.changed; + } + return _RewriteOutcome.noChangesNeeded; + } + + /// Returns all pubspecs anywhere under the packages directory. + Future> _getAllPubspecs() => packagesDir.parent + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => + entity is File && p.basename(entity.path) == 'pubspec.yaml') + .map((FileSystemEntity file) => file as File) + .toList(); + + /// Returns all packages that have non-breaking published changes (i.e., a + /// minor or bugfix version change) relative to the git comparison base. + /// + /// Prints status information about what was checked for ease of auditing logs + /// in CI. + Future> _getNonBreakingUpdatePackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print('Finding changed packages relative to "$baseSha"...'); + + final Set changedPackages = {}; + for (final String changedPath in await gitVersionFinder.getChangedFiles()) { + // Git output always uses Posix paths. + final List allComponents = p.posix.split(changedPath); + // Only pubspec changes are potential publishing events. + if (allComponents.last != 'pubspec.yaml' || + allComponents.contains('example')) { + continue; + } + if (!allComponents.contains(packagesDir.basename)) { + print(' Skipping $changedPath; not in packages directory.'); + continue; + } + final RepositoryPackage package = + RepositoryPackage(packagesDir.fileSystem.file(changedPath).parent); + // Ignored deleted packages, as they won't be published. + if (!package.pubspecFile.existsSync()) { + final String directoryName = p.posix.joinAll(path.split(path.relative( + package.directory.absolute.path, + from: packagesDir.path))); + print(' Skipping $directoryName; deleted.'); + continue; + } + final String packageName = package.parsePubspec().name; + if (!await _hasNonBreakingVersionChange(package)) { + // Log packages that had pubspec changes but weren't included for ease + // of auditing CI. + print(' Skipping $packageName; no non-breaking version change.'); + continue; + } + changedPackages.add(packageName); + } + return changedPackages; + } + + Future _hasNonBreakingVersionChange(RepositoryPackage package) async { + final Pubspec pubspec = package.parsePubspec(); + if (pubspec.publishTo == 'none') { + return false; + } + + final String pubspecGitPath = p.posix.joinAll(path.split(path.relative( + package.pubspecFile.absolute.path, + from: (await gitDir).path))); + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final Version? previousVersion = + await gitVersionFinder.getPackageVersion(pubspecGitPath); + if (previousVersion == null) { + // The plugin is new, so nothing can be depending on it yet. + return false; + } + final Version newVersion = pubspec.version!; + if ((newVersion.major > 0 && newVersion.major != previousVersion.major) || + (newVersion.major == 0 && newVersion.minor != previousVersion.minor)) { + // Breaking changes aren't targetted since they won't be picked up + // automatically. + return false; + } + return newVersion != previousVersion; + } +} diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 4911b4aeb156..81b13cbb75e2 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -5,6 +5,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; +import 'common/cmake.dart'; import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; @@ -16,9 +17,9 @@ import 'common/xcode.dart'; const String _unitTestFlag = 'unit'; const String _integrationTestFlag = 'integration'; -const String _iosDestinationFlag = 'ios-destination'; +const String _iOSDestinationFlag = 'ios-destination'; -const int _exitNoIosSimulators = 3; +const int _exitNoIOSSimulators = 3; /// The command to run native tests for plugins: /// - iOS and macOS: XCTests (XCUnitTest and XCUITest) @@ -33,17 +34,17 @@ class NativeTestCommand extends PackageLoopingCommand { }) : _xcode = Xcode(processRunner: processRunner, log: true), super(packagesDir, processRunner: processRunner, platform: platform) { argParser.addOption( - _iosDestinationFlag, + _iOSDestinationFlag, help: 'Specify the destination when running iOS tests.\n' 'This is passed to the `-destination` argument in the xcodebuild command.\n' 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT ' 'for details on how to specify the destination.', ); - argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); - argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); - argParser.addFlag(kPlatformLinux, help: 'Runs Linux tests'); - argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); - argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); + argParser.addFlag(platformAndroid, help: 'Runs Android tests'); + argParser.addFlag(platformIOS, help: 'Runs iOS tests'); + argParser.addFlag(platformLinux, help: 'Runs Linux tests'); + argParser.addFlag(platformMacOS, help: 'Runs macOS tests'); + argParser.addFlag(platformWindows, help: 'Runs Windows tests'); // By default, both unit tests and integration tests are run, but provide // flags to disable one or the other. @@ -54,7 +55,7 @@ class NativeTestCommand extends PackageLoopingCommand { } // The device destination flags for iOS tests. - List _iosDestinationFlags = []; + List _iOSDestinationFlags = []; final Xcode _xcode; @@ -83,11 +84,11 @@ this command. @override Future initializeRun() async { _platforms = { - kPlatformAndroid: _PlatformDetails('Android', _testAndroid), - kPlatformIos: _PlatformDetails('iOS', _testIos), - kPlatformLinux: _PlatformDetails('Linux', _testLinux), - kPlatformMacos: _PlatformDetails('macOS', _testMacOS), - kPlatformWindows: _PlatformDetails('Windows', _testWindows), + platformAndroid: _PlatformDetails('Android', _testAndroid), + platformIOS: _PlatformDetails('iOS', _testIOS), + platformLinux: _PlatformDetails('Linux', _testLinux), + platformMacOS: _PlatformDetails('macOS', _testMacOS), + platformWindows: _PlatformDetails('Windows', _testWindows), }; _requestedPlatforms = _platforms.keys .where((String platform) => getBoolArg(platform)) @@ -104,29 +105,29 @@ this command. throw ToolExit(exitInvalidArguments); } - if (getBoolArg(kPlatformWindows) && getBoolArg(_integrationTestFlag)) { + if (getBoolArg(platformWindows) && getBoolArg(_integrationTestFlag)) { logWarning('This command currently only supports unit tests for Windows. ' 'See https://github.com/flutter/flutter/issues/70233.'); } - if (getBoolArg(kPlatformLinux) && getBoolArg(_integrationTestFlag)) { + if (getBoolArg(platformLinux) && getBoolArg(_integrationTestFlag)) { logWarning('This command currently only supports unit tests for Linux. ' 'See https://github.com/flutter/flutter/issues/70235.'); } // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { - String destination = getStringArg(_iosDestinationFlag); + String destination = getStringArg(_iOSDestinationFlag); if (destination.isEmpty) { final String? simulatorId = await _xcode.findBestAvailableIphoneSimulator(); if (simulatorId == null) { printError('Cannot find any available iOS simulators.'); - throw ToolExit(_exitNoIosSimulators); + throw ToolExit(_exitNoIOSSimulators); } destination = 'id=$simulatorId'; } - _iosDestinationFlags = [ + _iOSDestinationFlags = [ '-destination', destination, ]; @@ -197,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'); @@ -268,7 +269,7 @@ this command. _printRunningExampleTestsMessage(example, 'Android'); final GradleProject project = GradleProject( - example.directory, + example, processRunner: processRunner, platform: platform, ); @@ -332,9 +333,9 @@ this command. return _PlatformResult(RunState.succeeded); } - Future<_PlatformResult> _testIos(RepositoryPackage plugin, _TestMode mode) { + Future<_PlatformResult> _testIOS(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'iOS', mode, - extraFlags: _iosDestinationFlags); + extraFlags: _iOSDestinationFlags); } Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { @@ -456,8 +457,8 @@ this command. file.basename.endsWith('_tests.exe'); } - return _runGoogleTestTests(plugin, - buildDirectoryName: 'windows', isTestBinary: isTestBinary); + return _runGoogleTestTests(plugin, 'Windows', 'Debug', + isTestBinary: isTestBinary); } Future<_PlatformResult> _testLinux( @@ -471,8 +472,16 @@ this command. file.basename.endsWith('_tests'); } - return _runGoogleTestTests(plugin, - buildDirectoryName: 'linux', isTestBinary: isTestBinary); + // Since Linux uses a single-config generator, building-examples only + // generates the build files for release, so the tests have to be run in + // release mode as well. + // + // TODO(stuartmorgan): Consider adding a command to `flutter` that would + // generate build files without doing a build, and using that instead of + // relying on running build-examples. See + // https://github.com/flutter/flutter/issues/93407. + return _runGoogleTestTests(plugin, 'Linux', 'Release', + isTestBinary: isTestBinary); } /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s @@ -482,38 +491,66 @@ this command. /// The binaries are assumed to be Google Test test binaries, thus returning /// zero for success and non-zero for failure. Future<_PlatformResult> _runGoogleTestTests( - RepositoryPackage plugin, { - required String buildDirectoryName, + RepositoryPackage plugin, + String platformName, + String buildMode, { required bool Function(File) isTestBinary, }) async { final List testBinaries = []; + bool hasMissingBuild = false; + bool buildFailed = false; for (final RepositoryPackage example in plugin.getExamples()) { - final Directory buildDir = example.directory - .childDirectory('build') - .childDirectory(buildDirectoryName); - if (!buildDir.existsSync()) { + final CMakeProject project = CMakeProject(example.directory, + buildMode: buildMode, + processRunner: processRunner, + platform: platform); + if (!project.isConfigured()) { + printError('ERROR: Run "flutter build" on ${example.displayName}, ' + 'or run this tool\'s "build-examples" command, for the target ' + 'platform before executing tests.'); + hasMissingBuild = true; continue; } - testBinaries.addAll(buildDir + + // By repository convention, example projects create an aggregate target + // called 'unit_tests' that builds all unit tests (usually just an alias + // for a specific test target). + final int exitCode = await project.runBuild('unit_tests'); + if (exitCode != 0) { + printError('${example.displayName} unit tests failed to build.'); + buildFailed = true; + } + + testBinaries.addAll(project.buildDirectory .listSync(recursive: true) .whereType() .where(isTestBinary) .where((File file) { - // Only run the release build of the unit tests, to avoid running the - // same tests multiple times. Release is used rather than debug since - // `build-examples` builds release versions. + // Only run the `buildMode` build of the unit tests, to avoid running + // the same tests multiple times. final List components = path.split(file.path); - return components.contains('release') || components.contains('Release'); + return components.contains(buildMode) || + components.contains(buildMode.toLowerCase()); })); } + if (hasMissingBuild) { + return _PlatformResult(RunState.failed, + error: 'Examples must be built before testing.'); + } + + if (buildFailed) { + return _PlatformResult(RunState.failed, + error: 'Failed to build $platformName unit tests.'); + } + if (testBinaries.isEmpty) { final String binaryExtension = platform.isWindows ? '.exe' : ''; printError( 'No test binaries found. At least one *_test(s)$binaryExtension ' 'binary should be built by the example(s)'); return _PlatformResult(RunState.failed, - error: 'No $buildDirectoryName unit tests found'); + error: 'No $platformName unit tests found'); } bool passing = true; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 563e0904552a..af8eac2257eb 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'; @@ -123,13 +122,12 @@ class PublishCheckCommand extends PackageLoopingCommand { } Pubspec? _tryParsePubspec(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - try { - return Pubspec.parse(pubspecFile.readAsStringSync()); + return package.parsePubspec(); } on Exception catch (exception) { print( - 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}', + 'Failed to parse `pubspec.yaml` at ${package.pubspecFile.path}: ' + '$exception', ); return null; } @@ -248,12 +246,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_plugin_command.dart index 4fdecf603eec..7aa70bd4fd1c 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -13,7 +13,6 @@ 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'; @@ -122,6 +121,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 +151,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 ==============='); } @@ -217,16 +223,15 @@ class PublishPluginCommand extends PackageLoopingCommand { /// In cases where a non-null result is returned, that should be returned /// as the final result for the package, without further processing. Future _checkNeedsRelease(RepositoryPackage package) async { - final File pubspecFile = package.pubspecFile; - if (!pubspecFile.existsSync()) { + if (!package.pubspecFile.existsSync()) { logWarning(''' -The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. +The pubspec file for ${package.displayName} does not exist, so no publishing will happen. Safe to ignore if the package is deleted in this commit. '''); return PackageResult.skip('package deleted'); } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = package.parsePubspec(); if (pubspec.name == 'flutter_plugin_tools') { // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. @@ -335,22 +340,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 605a8aa83a30..79ef1e1d3e5e 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -5,7 +5,7 @@ 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'; import 'common/package_looping_command.dart'; @@ -39,6 +39,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { 'flutter:', 'dependencies:', 'dev_dependencies:', + 'false_secrets:', ]; static const List _majorPackageSections = [ @@ -46,6 +47,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { 'dependencies:', 'dev_dependencies:', 'flutter:', + 'false_secrets:', ]; static const String _expectedIssueLinkFormat = @@ -62,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 { @@ -98,9 +101,17 @@ class PubspecCheckCommand extends PackageLoopingCommand { } if (isPlugin) { - final String? error = _checkForImplementsError(pubspec, package: package); - if (error != null) { - printError('$indentation$error'); + final String? implementsError = + _checkForImplementsError(pubspec, package: package); + if (implementsError != null) { + printError('$indentation$implementsError'); + passing = false; + } + + final String? defaultPackageError = + _checkForDefaultPackageError(pubspec, package: package); + if (defaultPackageError != null) { + printError('$indentation$defaultPackageError'); passing = false; } } @@ -124,6 +135,18 @@ class PubspecCheckCommand extends PackageLoopingCommand { '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } + + // Don't check descriptions for federated package components other than + // the app-facing package, since they are unlisted, and are expected to + // have short descriptions. + if (!package.isPlatformInterface && !package.isPlatformImplementation) { + final String? descriptionError = + _checkDescription(pubspec, package: package); + if (descriptionError != null) { + printError('$indentation$descriptionError'); + passing = false; + } + } } return passing; @@ -163,11 +186,16 @@ class PubspecCheckCommand extends PackageLoopingCommand { errorMessages.add('Missing "repository"'); } else { final String relativePackagePath = - path.relative(package.path, from: packagesDir.parent.path); + getRelativePosixPath(package.directory, from: packagesDir.parent); if (!pubspec.repository!.path.endsWith(relativePackagePath)) { errorMessages .add('The "repository" link should end with the package path.'); } + + if (pubspec.repository!.path.contains('/master/')) { + errorMessages + .add('The "repository" link should use "main", not "master".'); + } } if (pubspec.homepage != null) { @@ -178,11 +206,33 @@ class PubspecCheckCommand extends PackageLoopingCommand { return errorMessages; } + // Validates the "description" field for a package, returning an error + // string if there are any issues. + String? _checkDescription( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final String? description = pubspec.description; + if (description == null) { + return 'Missing "description"'; + } + + if (description.length < 60) { + return '"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + if (description.length > 180) { + return '"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + return null; + } + bool _checkIssueLink(Pubspec pubspec) { return pubspec.issueTracker ?.toString() - .startsWith(_expectedIssueLinkFormat) == - true; + .startsWith(_expectedIssueLinkFormat) ?? + false; } // Validates the "implements" keyword for a plugin, returning an error @@ -207,6 +257,49 @@ class PubspecCheckCommand extends PackageLoopingCommand { return null; } + // Validates any "default_package" entries a plugin, returning an error + // string if there are any issues. + // + // Should only be called on plugin packages. + String? _checkForDefaultPackageError( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; + if (platformsEntry == null) { + logWarning('Does not implement any platforms'); + return null; + } + final YamlMap platforms = platformsEntry as YamlMap; + final String packageName = package.directory.basename; + + // Validate that the default_package entries look correct (e.g., no typos). + final Set defaultPackages = {}; + for (final MapEntry platformEntry in platforms.entries) { + final String? defaultPackage = + platformEntry.value['default_package'] as String?; + if (defaultPackage != null) { + defaultPackages.add(defaultPackage); + if (!defaultPackage.startsWith('${packageName}_')) { + return '"$defaultPackage" is not an expected implementation name ' + 'for "$packageName"'; + } + } + } + + // Validate that all default_packages are also dependencies. + final Iterable dependencies = pubspec.dependencies.keys; + final Iterable missingPackages = defaultPackages + .where((String package) => !dependencies.contains(package)); + if (missingPackages.isNotEmpty) { + return 'The following default_packages are missing ' + 'corresponding dependencies:\n' + ' ${missingPackages.join('\n ')}'; + } + + return null; + } + // Returns true if [packageName] appears to be an implementation package // according to repository conventions. bool _isImplementationPackage(RepositoryPackage package) { 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..6e79b736781f --- /dev/null +++ b/script/tool/lib/src/readme_check_command.dart @@ -0,0 +1,271 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to enforce 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); + if (blockValidationError != null) { + errors.add(blockValidationError); + } + + if (_containsTemplateBoilerplate(readmeLines)) { + printError('${indentation}The boilerplate section about getting started ' + 'with Flutter should not be left in.'); + errors.add('Contains template boilerplate'); + } + + // 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) { + final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); + final List 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') { + const String excerptTagStart = ' ' + 'tag on the previous line, and ensure that a build.excerpt.yaml is ' + 'configured for the source example.\n'); + 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; + } + + /// Returns true if the README still has the boilerplate from the + /// `flutter create` templates. + bool _containsTemplateBoilerplate(List readmeLines) { + return readmeLines.any((String line) => + line.contains('For help getting started with Flutter')); + } +} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 5a0b43d3b223..5101b8f19e7e 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -24,7 +24,7 @@ class TestCommand extends PackageLoopingCommand { defaultsTo: '', help: 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' - 'See https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md ' + 'See https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md ' 'for details.', ); } @@ -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); @@ -62,7 +66,7 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (pluginSupportsPlatform(kPlatformWeb, package)) '--platform=chrome', + if (pluginSupportsPlatform(platformWeb, package)) '--platform=chrome', ], workingDir: package.directory, ); @@ -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..320a3c596323 --- /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:flutter_plugin_tools/src/common/core.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/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..b998615ead17 --- /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:flutter_plugin_tools/src/common/core.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml_edit/yaml_edit.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 = 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 6b49c40d66bb..246382dfae00 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -9,15 +9,17 @@ 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. @@ -29,8 +31,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. @@ -38,8 +40,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, @@ -48,8 +53,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. @@ -73,17 +78,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; } @@ -108,16 +113,66 @@ class VersionCheckCommand extends PackageLoopingCommand { argParser.addFlag( _againstPubFlag, help: 'Whether the version check should run against the version on pub.\n' - 'Defaults to false, which means the version check only run against the previous version in code.', + '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 ' + '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.', + 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.', + hide: true); } static const String _againstPubFlag = 'against-pub'; + static const String _changeDescriptionFile = 'change-description-file'; + 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 + /// change to a platform interface. + static const String _breakingChangeJustificationMarker = + '## Breaking change justification'; + + /// 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 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:'; final PubVersionFinder _pubVersionFinder; + late final GitVersionFinder _gitVersionFinder; + late final String _mergeBase; + late final List _changedFiles; + + late final String _changeDescription = _loadChangeDescription(); + @override final String name = 'version-check'; @@ -131,7 +186,11 @@ class VersionCheckCommand extends PackageLoopingCommand { bool get hasLongOutput => false; @override - Future initializeRun() async {} + Future initializeRun() async { + _gitVersionFinder = await retrieveVersionFinder(); + _mergeBase = await _gitVersionFinder.getBaseSha(); + _changedFiles = await _gitVersionFinder.getChangedFiles(); + } @override Future runForPackage(RepositoryPackage package) async { @@ -163,7 +222,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: @@ -177,10 +237,21 @@ class VersionCheckCommand extends PackageLoopingCommand { } if (!(await _validateChangelogVersion(package, - pubspec: pubspec, pubspecVersionChanged: versionChanged))) { + pubspec: pubspec, pubspecVersionState: versionState))) { errors.add('CHANGELOG.md failed validation.'); } + // If there are no other issues, make sure that there isn't a missing + // change to the version and/or CHANGELOG. + if (getBoolArg(_checkForMissingChanges) && + !versionChanged && + errors.isEmpty) { + final String? error = await _checkForMissingChangeError(package); + if (error != null) { + errors.add(error); + } + } + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); @@ -214,10 +285,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } /// Returns the version of [package] from git at the base comparison hash. - Future _getPreviousVersionFromGit( - RepositoryPackage package, { - required GitVersionFinder gitVersionFinder, - }) async { + Future _getPreviousVersionFromGit(RepositoryPackage package) async { final File pubspecFile = package.pubspecFile; final String relativePath = path.relative(pubspecFile.absolute.path, from: (await gitDir).path); @@ -225,7 +293,8 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final String gitPath = path.style == p.Style.windows ? p.posix.joinAll(path.split(relativePath)) : relativePath; - return await gitVersionFinder.getPackageVersion(gitPath); + return await _gitVersionFinder.getPackageVersion(gitPath, + gitRef: _mergeBase); } /// Returns the state of the verison of [package] relative to the comparison @@ -237,7 +306,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // This method isn't called unless `version` is non-null. final Version currentVersion = pubspec.version!; Version? previousVersion; + String previousVersionSource; if (getBoolArg(_againstPubFlag)) { + previousVersionSource = 'pub'; previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); if (previousVersion == null) { return _CurrentVersionState.unknown; @@ -247,17 +318,16 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '$indentation${pubspec.name}: Current largest version on pub: $previousVersion'); } } else { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - previousVersion = await _getPreviousVersionFromGit(package, - gitVersionFinder: gitVersionFinder) ?? - Version.none; + previousVersionSource = _mergeBase; + previousVersion = + await _getPreviousVersionFromGit(package) ?? Version.none; } if (previousVersion == Version.none) { print('${indentation}Unable to find previous version ' '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( '${indentation}If this plugin is not new, something has gone wrong.'); - return _CurrentVersionState.validChange; // Assume new, thus valid. + return _CurrentVersionState.validIncrease; // Assume new, thus valid. } if (previousVersion == currentVersion) { @@ -267,42 +337,48 @@ ${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 { - final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; printError('${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' + '${indentation}HEAD: $currentVersion, $previousVersionSource: $previousVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'); return _CurrentVersionState.invalidChange; } - final bool isPlatformInterface = - pubspec.name.endsWith('_platform_interface'); - // TODO(stuartmorgan): Relax this check. See - // https://github.com/flutter/flutter/issues/85391 - if (isPlatformInterface && - 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 strongly discouraged.\n'); + '${indentation}Breaking changes to platform interfaces are not ' + 'allowed without explicit justification.\n' + '${indentation}See ' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' + 'for more information.'); return _CurrentVersionState.invalidChange; } - return _CurrentVersionState.validChange; + + return _CurrentVersionState.validIncrease; } /// Checks whether or not [package]'s CHANGELOG's versioning is correct, @@ -313,13 +389,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; @@ -334,22 +410,24 @@ ${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; } print( '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating - // CHANGELOG. That means the next version entry in the CHANGELOG pass the - // normal validation. + // CHANGELOG. That means the next version entry in the CHANGELOG should + // pass the normal validation. + versionString = null; while (iterator.moveNext()) { if (iterator.current.trim().startsWith('## ')) { versionString = iterator.current.trim().split(' ').last; @@ -358,11 +436,19 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} } } - final Version? fromChangeLog = - versionString == null ? null : Version.parse(versionString); - if (fromChangeLog == null) { - printError( - '${indentation}Cannot find version on the first line CHANGELOG.md'); + if (versionString == null) { + printError('${indentation}Unable to find a version in CHANGELOG.md'); + print('${indentation}The current version should be on a line starting ' + 'with "## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'); + return false; + } + + final Version fromChangeLog; + try { + fromChangeLog = Version.parse(versionString); + } on FormatException { + printError('"$versionString" could not be parsed as a version.'); return false; } @@ -388,14 +474,135 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. } Pubspec? _tryParsePubspec(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - try { - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + final Pubspec pubspec = package.parsePubspec(); return pubspec; } on Exception catch (exception) { printError('${indentation}Failed to parse `pubspec.yaml`: $exception}'); return null; } } + + /// Checks whether the current breaking change to [package] should be allowed, + /// logging extra information for auditing when allowing unusual cases. + bool _validateBreakingChange(RepositoryPackage package) { + // Only platform interfaces have breaking change restrictions. + if (!package.isPlatformInterface) { + return true; + } + + if (getBoolArg(_ignorePlatformInterfaceBreaks)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to --$_ignorePlatformInterfaceBreaks'); + return true; + } + + if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to "$_breakingChangeJustificationMarker" in the change ' + 'description.'); + return true; + } + + return false; + } + + String _getChangeDescription() => _changeDescription; + + /// 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 ''; + } + final File file = packagesDir.fileSystem.file(path); + if (!file.existsSync()) { + printError('${indentation}No such file: $path'); + throw ToolExit(_exitMissingChangeDescriptionFile); + } + return file.readAsStringSync(); + } + + /// Returns true if the given version transition should be allowed. + bool _shouldAllowVersionChange( + {required Version oldVersion, required Version newVersion}) { + // Get the non-pre-release next version mapping. + final Map allowedNextVersions = + getAllowedNextVersions(oldVersion, newVersion: newVersion); + + if (allowedNextVersions.containsKey(newVersion)) { + return true; + } + // Allow a pre-release version of a version that would be a valid + // transition. + if (newVersion.isPreRelease) { + final Version targetReleaseVersion = newVersion.nextPatch; + if (allowedNextVersions.containsKey(targetReleaseVersion)) { + return true; + } + } + return false; + } + + /// Returns an error string if the changes to this package should have + /// resulted in a version change, or shoud have resulted in a CHANGELOG change + /// but didn't. + /// + /// This should only be called if the version did not change. + Future _checkForMissingChangeError(RepositoryPackage package) async { + // Find the relative path to the current package, as it would appear at the + // beginning of a path reported by getChangedFiles() (which always uses + // Posix paths). + final Directory gitRoot = + packagesDir.fileSystem.directory((await gitDir).path); + final String relativePackagePath = + getRelativePosixPath(package.directory, from: gitRoot); + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: _changedFiles, relativePackagePath: relativePackagePath); + + if (!state.hasChanges) { + return null; + } + + if (state.needsVersionChange) { + if (_getChangeDescription().split('\n').any((String line) => + line.startsWith(_missingVersionChangeJustificationMarker))) { + logWarning('Ignoring lack of version change due to ' + '"$_missingVersionChangeJustificationMarker" in the ' + 'change description.'); + } 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.'); + return 'Missing version change'; + } + } + + if (!state.hasChangelogChange) { + if (_getChangeDescription().split('\n').any((String line) => + line.startsWith(_missingChangelogChangeJustificationMarker))) { + logWarning('Ignoring lack of CHANGELOG update due to ' + '"$_missingChangelogChangeJustificationMarker" in the ' + 'change description.'); + } else { + printError( + '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.'); + return 'Missing CHANGELOG change'; + } + } + + return null; + } } diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 3d34dab9f087..a81bf15477af 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -21,10 +21,22 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { Platform platform = const LocalPlatform(), }) : _xcode = Xcode(processRunner: processRunner, log: true), super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(kPlatformIos, help: 'Analyze iOS'); - argParser.addFlag(kPlatformMacos, help: 'Analyze macOS'); + 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 @@ -36,7 +48,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { @override Future initializeRun() async { - if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) { + if (!(getBoolArg(platformIOS) || getBoolArg(platformMacOS))) { printError('At least one platform flag must be provided.'); throw ToolExit(exitInvalidArguments); } @@ -44,28 +56,37 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final bool testIos = getBoolArg(kPlatformIos) && - pluginSupportsPlatform(kPlatformIos, package, + final bool testIOS = getBoolArg(platformIOS) && + pluginSupportsPlatform(platformIOS, package, requiredMode: PlatformSupport.inline); - final bool testMacos = getBoolArg(kPlatformMacos) && - pluginSupportsPlatform(kPlatformMacos, package, + final bool testMacOS = getBoolArg(platformMacOS) && + pluginSupportsPlatform(platformMacOS, package, requiredMode: PlatformSupport.inline); final bool multiplePlatformsRequested = - getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); - if (!(testIos || testMacos)) { + getBoolArg(platformIOS) && getBoolArg(platformMacOS); + if (!(testIOS || testMacOS)) { return PackageResult.skip('Not implemented for target platform(s).'); } + final String minIOSVersion = getStringArg(_minIOSVersionArg); + final String minMacOSVersion = getStringArg(_minMacOSVersionArg); + final List failures = []; - if (testIos && + 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 689618f06123..b8233de11b41 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/master/script/tool -version: 0.7.1 +repository: https://github.com/flutter/plugins/tree/main/script/tool +version: 0.8.8 dependencies: args: ^2.1.0 @@ -21,12 +21,12 @@ 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 matcher: ^0.12.10 mockito: ^5.0.7 - pedantic: ^1.11.0 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 502fa9a0634c..a4a47a221189 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 plugin1 = createFakePlugin('a', packagesDir); + final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], plugin1.path), ProcessCall( - 'flutter', const ['packages', 'get'], plugin1Dir.path), + 'dart', const ['analyze', '--fatal-infos'], plugin1.path), + ProcessCall('flutter', const ['pub', 'get'], plugin2.path), ProcessCall( - 'flutter', const ['packages', 'get'], plugin2Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin1Dir.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin2Dir.path), + '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,36 @@ 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("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 +122,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, ), ]), ); @@ -160,7 +180,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 +189,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 +207,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 +237,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; @@ -253,14 +283,14 @@ void main() { }); // Ensure that the command used to analyze flutter/plugins in the Dart repo: - // https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh + // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh // continues to work. // // DO NOT remove or modify this test without a coordination plan in place to // modify the script above, as it is run from source, but out-of-repo. // Contact stuartmorgan or devoncarew for assistance. test('Dart repo analyze command works', () async { - final 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 +309,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 c3b0cb9d5cd1..420b3b1161db 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -57,13 +57,13 @@ void main() { test('fails if building fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }); processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess(exitCode: 1) // flutter packages get + MockProcess(exitCode: 1) // flutter pub get ]; Error? commandError; @@ -86,13 +86,13 @@ void main() { createFakePlugin('plugin', packagesDir, examples: [], platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); 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: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformWindows: 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: { - kPlatformWindows: 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + 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']); @@ -547,7 +457,7 @@ void main() { test('logs skipped platforms', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint( @@ -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,7 +499,7 @@ void main() { 'ios', '--no-codesign', ], - packageDirectory.childDirectory('example').path), + getExampleDir(package).path), ])); }); @@ -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: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + 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/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart index f1f40b5e0035..ad1a26ffc165 100644 --- a/script/tool/test/common/git_version_finder_test.dart +++ b/script/tool/test/common/git_version_finder_test.dart @@ -14,7 +14,7 @@ void main() { late List?> gitDirCommands; late String gitDiffResponse; late MockGitDir gitDir; - String? mergeBaseResponse; + String mergeBaseResponse = ''; setUp(() { gitDirCommands = ?>[]; @@ -74,7 +74,7 @@ file2/file2.cc final GitVersionFinder finder = GitVersionFinder(gitDir, null); await finder.getChangedFiles(); verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); + ['diff', '--name-only', mergeBaseResponse, 'HEAD'])); }); test('use correct base sha if specified', () async { @@ -88,6 +88,18 @@ file2/file2.cc verify(gitDir .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); }); + + test('include uncommitted files if requested', () async { + const String customBaseSha = 'aklsjdcaskf12312'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); + await finder.getChangedFiles(includeUncommitted: true); + // The call should not have HEAD as a final argument like the default diff. + verify(gitDir.runCommand(['diff', '--name-only', customBaseSha])); + }); } class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart 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/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 7cf03960a74d..ec2b9b9be232 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'; @@ -22,6 +21,7 @@ import '../util.dart'; import 'plugin_command_test.mocks.dart'; // Constants for colorized output start and end. +const String _startElapsedTimeColor = '\x1B[90m'; const String _startErrorColor = '\x1B[31m'; const String _startHeadingColor = '\x1B[36m'; const String _startSkipColor = '\x1B[90m'; @@ -30,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. @@ -39,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; @@ -59,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, @@ -83,7 +122,7 @@ void main() { packagesDir, platform: mockPlatform, hasLongOutput: hasLongOutput, - includeSubpackages: includeSubpackages, + packageLoopingType: packageLoopingType, failsDuringInit: failsDuringInit, warnsDuringInit: warnsDuringInit, warnsDuringCleanup: warnsDuringCleanup, @@ -121,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); @@ -146,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); @@ -172,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); @@ -183,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(); @@ -194,46 +236,170 @@ 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, + getExampleDir(included).path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + expect(examples.length, 2); + for (final RepositoryPackage example in examples) { + expect(command.checkedPackages, isNot(contains(example.path))); + } + expect(command.checkedPackages, isNot(contains(subpackage.path))); + }); + + test('excludes examples when main package is excluded', () async { + final RepositoryPackage excluded = createFakePlugin( + 'a_plugin', packagesDir, + examples: ['example1', 'example2']); + final RepositoryPackage included = + createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeExamples); await runCommand(command, arguments: ['--exclude=a_plugin']); + final Iterable examples = excluded.getExamples(); + expect( command.checkedPackages, unorderedEquals([ included.path, - 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))); + } + }); + + 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, + isFlutter: false, dartConstraint: '>=2.17.0 <3.0.0'); + final RepositoryPackage included = createFakePackage( + 'a_package', packagesDir, + isFlutter: false, dartConstraint: '>=2.14.0 <3.0.0'); + + 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', + ])); }); }); @@ -272,6 +438,46 @@ void main() { ])); }); + test('prints timing info in long-form output when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = + await runCommand(command, arguments: ['--log-timing']); + + const String separator = + '============================================================'; + expect( + output, + containsAllInOrder([ + '$_startHeadingColor\n$separator\n|| Running for package_a [@0:00]\n$separator\n$_endColor', + '$_startElapsedTimeColor\n[package_a completed in 0m 0s]$_endColor', + '$_startHeadingColor\n$separator\n|| Running for package_b [@0:00]\n$separator\n$_endColor', + '$_startElapsedTimeColor\n[package_b completed in 0m 0s]$_endColor', + ])); + }); + + test('prints timing info in short-form output when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--log-timing']); + + expect( + output, + containsAllInOrder([ + '$_startHeadingColor[0:00] Running for package_a...$_endColor', + '$_startHeadingColor[0:00] Running for package_b...$_endColor', + ])); + // Short-form output should not include elapsed time. + expect(output, isNot(contains('[package_a completed in 0m 0s]'))); + }); + test('shows the success message when nothing fails', () async { createFakePackage('package_a', packagesDir); createFakePackage('package_b', packagesDir); @@ -291,13 +497,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); @@ -321,13 +527,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, @@ -354,17 +560,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); @@ -410,8 +614,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); @@ -444,10 +650,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 = @@ -466,10 +672,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); @@ -490,23 +696,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 = @@ -546,23 +759,30 @@ 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 = @@ -638,7 +858,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, @@ -664,7 +884,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..cc9116a9ea25 --- /dev/null +++ b/script/tool/test/common/package_state_utils_test.dart @@ -0,0 +1,140 @@ +// Copyright 2013 The Flutter Authors. 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/package_state_utils.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('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 = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_package'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, 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 = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_package/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + expect(state.hasChangelogChange, false); + }); + + test('does not report version change exempt changes', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/android/lint-baseline.xml', + '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', + 'packages/a_plugin/tool/a_development_tool.dart', + 'packages/a_plugin/CHANGELOG.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, 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 = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example main', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/main.dart', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, 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 = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + + test('requires a version change for example example.md', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir); + + const List changedFiles = [ + 'packages/a_plugin/example/lib/example.md', + ]; + + final PackageChangeState state = checkPackageChangeState(package, + changedPaths: changedFiles, + relativePackagePath: 'packages/a_plugin/'); + + expect(state.hasChanges, true); + expect(state.needsVersionChange, true); + }); + }); +} diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 13724e26e5f8..8c6b38682418 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -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', @@ -180,6 +208,103 @@ void main() { expect(command.plugins, unorderedEquals([])); }); + test( + 'explicitly specifying the plugin (group) name of a federated plugin ' + 'should include all plugins in the group', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + final RepositoryPackage appFacingPackage = + createFakePlugin('plugin1', pluginGroup); + final RepositoryPackage platformInterfacePackage = + createFakePlugin('plugin1_platform_interface', pluginGroup); + final RepositoryPackage implementationPackage = + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint( + runner, ['sample', '--base-sha=main', '--packages=plugin1']); + + expect( + command.plugins, + unorderedEquals([ + appFacingPackage.path, + platformInterfacePackage.path, + implementationPackage.path + ])); + }); + + test( + 'specifying the app-facing package of a federated plugin using its ' + 'fully qualified name should include only that package', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + final RepositoryPackage appFacingPackage = + createFakePlugin('plugin1', pluginGroup); + createFakePlugin('plugin1_platform_interface', pluginGroup); + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--packages=plugin1/plugin1']); + + expect(command.plugins, unorderedEquals([appFacingPackage.path])); + }); + + test( + 'specifying a package of a federated plugin by its name should ' + 'include only that package', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory pluginGroup = packagesDir.childDirectory('plugin1'); + + createFakePlugin('plugin1', pluginGroup); + final RepositoryPackage platformInterfacePackage = + createFakePlugin('plugin1_platform_interface', pluginGroup); + createFakePlugin('plugin1_web', pluginGroup); + + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=main', + '--packages=plugin1_platform_interface' + ]); + + expect(command.plugins, + unorderedEquals([platformInterfacePackage.path])); + }); + + test('returns subpackages after the enclosing package', () async { + final SamplePluginCommand localCommand = SamplePluginCommand( + 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 { @@ -244,13 +369,12 @@ void main() { 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); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -262,13 +386,12 @@ void main() { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'AUTHORS'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -281,13 +404,12 @@ void main() { packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -300,13 +422,12 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -320,13 +441,12 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -340,13 +460,12 @@ script/tool_runner.sh packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -360,13 +479,12 @@ analysis_options.yaml packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -380,13 +498,12 @@ packages/plugin1/CHANGELOG packages/plugin1/CHANGELOG '''), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); - final Directory plugin2 = createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -396,19 +513,17 @@ packages/plugin1/CHANGELOG 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=master', - '--run-on-changed-packages' - ]); + final List output = await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect( output, containsAllInOrder([ contains( - 'Running for all packages that have changed relative to "master"'), + 'Running for all packages that have changed relative to "main"'), ])); expect(command.plugins, unorderedEquals([plugin1.path])); @@ -422,13 +537,11 @@ 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=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); @@ -441,14 +554,13 @@ 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=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path, plugin2.path])); @@ -464,43 +576,33 @@ 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); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); expect(command.plugins, unorderedEquals([plugin1.path])); }); test( - 'changing one plugin in a federated group should include all plugins in the group', + 'changing one plugin in a federated group should only include that plugin', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: ''' packages/plugin1/plugin1/plugin1.dart '''), ]; - final Directory plugin1 = + final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - final Directory plugin2 = createFakePlugin('plugin1_platform_interface', + createFakePlugin('plugin1_platform_interface', packagesDir.childDirectory('plugin1')); - final Directory plugin3 = createFakePlugin( - 'plugin1_web', packagesDir.childDirectory('plugin1')); - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=master', - '--run-on-changed-packages' - ]); + createFakePlugin('plugin1_web', packagesDir.childDirectory('plugin1')); + await runCapturingPrint(runner, + ['sample', '--base-sha=main', '--run-on-changed-packages']); - expect( - command.plugins, - unorderedEquals( - [plugin1.path, plugin2.path, plugin3.path])); + expect(command.plugins, unorderedEquals([plugin1.path])); }); test('--exclude flag works with --run-on-changed-packages', () async { @@ -511,20 +613,122 @@ 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); await runCapturingPrint(runner, [ 'sample', '--exclude=plugin2,plugin3', - '--base-sha=master', + '--base-sha=main', '--run-on-changed-packages' ]); expect(command.plugins, unorderedEquals([plugin1.path])); }); }); + + group('test run-on-dirty-packages', () { + test('no packages should be tested if there are no changes.', () async { + createFakePackage('a_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, unorderedEquals([])); + }); + + test( + 'no packages should be tested if there are no plugin related changes.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'AUTHORS'), + ]; + createFakePackage('a_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, unorderedEquals([])); + }); + + test('no packages should be tested even if special repo files change.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +.cirrus.yml +.ci.yaml +.ci/Dockerfile +.clang-format +analysis_options.yaml +script/tool_runner.sh +'''), + ]; + createFakePackage('a_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, unorderedEquals([])); + }); + + test('Only changed packages should be tested.', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/a_package/lib/a_package.dart'), + ]; + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); + createFakePlugin('b_package', packagesDir); + final List output = await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect( + output, + containsAllInOrder([ + contains( + 'Running for all packages that have uncommitted changes'), + ])); + + expect(command.plugins, unorderedEquals([packageA.path])); + }); + + test('multiple packages changed should test all the changed packages', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/lib/a_package.dart +packages/b_package/lib/src/foo.dart +'''), + ]; + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); + final RepositoryPackage packageB = + createFakePackage('b_package', packagesDir); + createFakePackage('c_package', packagesDir); + await runCapturingPrint( + runner, ['sample', '--run-on-dirty-packages']); + + expect(command.plugins, + unorderedEquals([packageA.path, packageB.path])); + }); + + test('honors --exclude flag', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/a_package/lib/a_package.dart +packages/b_package/lib/src/foo.dart +'''), + ]; + final RepositoryPackage packageA = + createFakePackage('a_package', packagesDir); + createFakePackage('b_package', packagesDir); + createFakePackage('c_package', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--exclude=b_package', + '--run-on-dirty-packages' + ]); + + expect(command.plugins, unorderedEquals([packageA.path])); + }); + }); }); group('--packages-for-branch', () { @@ -535,7 +739,8 @@ packages/plugin3/plugin3.dart processRunner.mockProcessesForExecutable['git-rev-parse'] = [ MockProcess(stdout: 'a-branch'), ]; - final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( @@ -549,6 +754,30 @@ packages/plugin3/plugin3.dart ])); }); + test('tests all packages on main', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'main'), + ]; + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on all packages'), + ])); + }); + test('tests all packages on master', () async { processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: 'packages/plugin1/plugin1.dart'), @@ -556,8 +785,10 @@ packages/plugin3/plugin3.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); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir); final List output = await runCapturingPrint( runner, ['sample', '--packages-for-branch']); @@ -597,18 +828,19 @@ packages/plugin3/plugin3.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), @@ -634,25 +866,26 @@ packages/plugin3/plugin3.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), ], @@ -677,7 +910,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); @@ -691,18 +924,19 @@ packages/plugin3/plugin3.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), ], ]; @@ -730,7 +964,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory packageDir) => packageDir.path) + .map((RepositoryPackage package) => package.path) .toList())); } }); @@ -743,11 +977,14 @@ class SamplePluginCommand extends PluginCommand { 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'; @@ -756,7 +993,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_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index ac619e2622e0..9c5ddc3f85b9 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,323 +21,239 @@ void main() { group('pluginSupportsPlatform', () { test('no platforms', () async { - final RepositoryPackage plugin = - RepositoryPackage(createFakePlugin('plugin', packagesDir)); + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformLinux, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformWeb, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); + expect(pluginSupportsPlatform(platformAndroid, plugin), isFalse); + expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformLinux, plugin), isFalse); + expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformWeb, plugin), isFalse); + expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); }); test('all platforms', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), - })); + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); - expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformMacos, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformWindows, plugin), isTrue); + expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(platformIOS, plugin), isTrue); + expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(platformMacOS, plugin), isTrue); + expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(platformWindows, plugin), isTrue); }); test('some platforms', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), - })); + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + }); - expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); - expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); + expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); + expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); }); test('inline plugins are only detected as inline', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), - })); + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.federated), isFalse); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.inline), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.federated), isFalse); }); test('federated plugins are only detected as federated', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.federated), - kPlatformIos: const PlatformDetails(PlatformSupport.federated), - kPlatformLinux: const PlatformDetails(PlatformSupport.federated), - kPlatformMacos: const PlatformDetails(PlatformSupport.federated), - kPlatformWeb: const PlatformDetails(PlatformSupport.federated), - kPlatformWindows: const PlatformDetails(PlatformSupport.federated), - })); + platformAndroid: const PlatformDetails(PlatformSupport.federated), + platformIOS: const PlatformDetails(PlatformSupport.federated), + platformLinux: const PlatformDetails(PlatformSupport.federated), + platformMacOS: const PlatformDetails(PlatformSupport.federated), + platformWeb: const PlatformDetails(PlatformSupport.federated), + platformWindows: const PlatformDetails(PlatformSupport.federated), + }); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformAndroid, plugin, + pluginSupportsPlatform(platformAndroid, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformIos, plugin, + pluginSupportsPlatform(platformIOS, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformLinux, plugin, + pluginSupportsPlatform(platformLinux, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformMacos, plugin, + pluginSupportsPlatform(platformMacOS, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformWeb, plugin, + pluginSupportsPlatform(platformWeb, plugin, requiredMode: PlatformSupport.inline), isFalse); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.federated), isTrue); expect( - pluginSupportsPlatform(kPlatformWindows, plugin, + pluginSupportsPlatform(platformWindows, plugin, requiredMode: PlatformSupport.inline), isFalse); }); - - test('windows without variants is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), - }, - )); - - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('windows with both variants matches win32 and winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32, platformVariantWinUwp], - ), - })); - - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); - - test('win32 plugin is only win32', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - kPlatformWindows: const PlatformDetails( - PlatformSupport.federated, - variants: [platformVariantWin32], - ), - })); - - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWin32), - isTrue); - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWinUwp), - isFalse); - }); - - test('winup plugin is only winuwp', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.federated, - variants: [platformVariantWinUwp]), - }, - )); - - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWin32), - isFalse); - expect( - pluginSupportsPlatform(kPlatformWindows, plugin, - variant: platformVariantWinUwp), - isTrue); - }); }); group('pluginHasNativeCodeForPlatform', () { test('returns false for web', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, - )); + ); - expect(pluginHasNativeCodeForPlatform(kPlatformWeb, plugin), isFalse); + 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: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }, - )); + ); - expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); }); test('returns true for a native+Dart plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + platformLinux: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: true, hasDartCode: true), }, - )); + ); - expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); }); test('returns false for a Dart-only plugin', () async { - final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + platformLinux: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, hasNativeCode: false, hasDartCode: true), }, - )); + ); - expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isFalse); }); }); } diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart index 5c5624312f51..dadfc8832997 100644 --- a/script/tool/test/common/repository_package_test.dart +++ b/script/tool/test/common/repository_package_test.dart @@ -4,7 +4,6 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -96,28 +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 RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); + expect(plugin.isFederated, false); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, false); + expect(plugin.isFederated, false); + }); + + test('handle app-facing packages', () { + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, true); + expect(plugin.isPlatformInterface, false); + expect(plugin.isPlatformImplementation, false); + }); + + test('handle platform interface packages', () { + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin_platform_interface', + packagesDir.childDirectory('a_plugin')); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, true); + expect(plugin.isPlatformImplementation, false); + }); + + test('handle platform implementation packages', () { + // A platform interface can end with anything, not just one of the known + // platform names, because of cases like webview_flutter_wkwebview. + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); + expect(plugin.isFederated, true); + expect(plugin.isAppFacing, false); + expect(plugin.isPlatformInterface, false); + expect(plugin.isPlatformImplementation, true); + }); + }); + + group('pubspec', () { + test('file', () async { + final RepositoryPackage plugin = + createFakePlugin('a_plugin', packagesDir); + + final File pubspecFile = plugin.pubspecFile; + + expect(pubspecFile.path, plugin.directory.childFile('pubspec.yaml').path); + }); + + test('parsing', () async { + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + + final Pubspec pubspec = plugin.parsePubspec(); + + expect(pubspec.name, 'a_plugin'); + }); + }); + + group('requiresFlutter', () { + test('returns true for Flutter package', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, isFlutter: true); + expect(package.requiresFlutter(), true); + }); + + test('returns false for non-Flutter package', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir, isFlutter: false); + 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..830dd59a8d42 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_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. +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/create_all_plugins_app_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'util.dart'; @@ -45,8 +48,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 +65,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,14 +77,30 @@ 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('handles --output-dir', () async { @@ -94,8 +111,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..54a1acf8b82b --- /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 = + createFakePlugin('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 = createFakePlugin( + '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 = createFakePlugin( + '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 = createFakePlugin( + '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 { + createFakePlugin('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 { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // 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 { + createFakePlugin('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 = + createFakePlugin('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 = + createFakePlugin('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 = createFakePlugin( + '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 { + createFakePlugin('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 { + createFakePlugin('a_package', packagesDir, extraFiles: [ + 'tool/run_tests.dart', + 'run_tests.sh', + ]); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // 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..a4c8693b2c21 --- /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/plugin_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 a7a1652c2fc2..0b6082098ae8 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -17,7 +17,7 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -const String _fakeIosDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; +const String _fakeIOSDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; const String _fakeAndroidDevice = 'emulator-1234'; void main() { @@ -42,7 +42,7 @@ void main() { }); void setMockFlutterDevicesOutput({ - bool hasIosDevice = true, + bool hasIOSDevice = true, bool hasAndroidDevice = true, bool includeBanner = false, }) { @@ -54,7 +54,7 @@ void main() { ╚════════════════════════════════════════════════════════════════════════════╝ '''; final List devices = [ - if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}', + if (hasIOSDevice) '{"id": "$_fakeIOSDevice", "targetPlatform": "ios"}', if (hasAndroidDevice) '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', ]; @@ -104,7 +104,7 @@ void main() { }); test('fails for iOS if no iOS devices are present', () async { - setMockFlutterDevicesOutput(hasIosDevice: false); + setMockFlutterDevicesOutput(hasIOSDevice: false); Error? commandError; final List output = await runCapturingPrint( @@ -128,9 +128,10 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', + 'example/ios/ios.m', ], platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -187,21 +188,22 @@ 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = @@ -225,7 +227,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--driver', 'test_driver/plugin_test.dart', '--target', @@ -243,10 +245,12 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -276,10 +280,12 @@ void main() { packagesDir, extraFiles: [ 'example/lib/main.dart', + 'example/android/android.java', + 'example/ios/ios.m', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: 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,15 +359,16 @@ 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = @@ -344,7 +392,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--driver', 'test_driver/integration_test.dart', '--target', @@ -356,7 +404,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--driver', 'test_driver/integration_test.dart', '--target', @@ -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: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + 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: [ @@ -473,12 +521,11 @@ void main() { 'example/macos/macos.swift', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -536,20 +583,71 @@ 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 = getExampleDir(plugin); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving a web plugin with CHROME_EXECUTABLE', () async { + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', + 'example/web/index.html', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); + + mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome'; final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -575,6 +673,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + '--chrome-binary=/path/to/chrome', '--driver', 'test_driver/plugin_test.dart', '--target', @@ -610,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: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final List output = await runCapturingPrint(runner, [ 'drive-examples', @@ -656,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: { - kPlatformWindows: 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); final List output = await runCapturingPrint(runner, [ @@ -749,7 +814,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -782,7 +847,7 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -829,21 +894,22 @@ 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); setMockFlutterDevicesOutput(); await runCapturingPrint(runner, [ @@ -862,7 +928,7 @@ void main() { const [ 'drive', '-d', - _fakeIosDevice, + _fakeIOSDevice, '--enable-experiment=exp1', '--driver', 'test_driver/plugin_test.dart', @@ -879,7 +945,7 @@ void main() { packagesDir, examples: [], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -909,9 +975,10 @@ void main() { extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', + 'example/web/index.html', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -941,9 +1008,10 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/integration_test.dart', + 'example/web/index.html', ], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -970,16 +1038,17 @@ 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: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); @@ -1011,8 +1080,7 @@ void main() { ]), ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); expect( processRunner.recordedCalls, orderedEquals([ @@ -1042,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 e23485fbc8b7..015a0eb634d9 100644 --- a/script/tool/test/federation_safety_check_command_test.dart +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -8,7 +8,6 @@ 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'; @@ -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), @@ -352,4 +348,64 @@ void main() { ]), ); }); + + test('handles top-level files that match federated package heuristics', + () async { + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); + + final String changedFileOutput = [ + // This should be picked up as a change to 'foo', and not crash. + plugin.directory.childFile('foo_bar.baz'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + ]), + ); + }); + + test('handles deletion of an entire plugin', () async { + // Simulate deletion, in the form of diffs for packages that don't exist in + // the filesystem. + final String changedFileOutput = [ + packagesDir.childDirectory('foo').childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo') + .childDirectory('lib') + .childFile('foo.dart'), + packagesDir + .childDirectory('foo_platform_interface') + .childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo_platform_interface') + .childDirectory('lib') + .childFile('foo.dart'), + packagesDir.childDirectory('foo_web').childFile('pubspec.yaml'), + packagesDir + .childDirectory('foo_web') + .childDirectory('lib') + .childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Ran for 0 package(s)'), + ]), + ); + }); } diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index e39ccf30b136..a41409394728 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -8,14 +8,16 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; void main() { - group('$FirebaseTestLabCommand', () { + group('FirebaseTestLabCommand', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; @@ -38,15 +40,34 @@ void main() { runner.addCommand(command); }); + void _writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, + {String runnerClass = 'FlutterTestRunner'}) { + childFileWithSubcomponents( + plugin.directory, p.posix.split(relativeFilePath)) + .writeAsStringSync(''' +@DartIntegrationTest +@RunWith($runnerClass.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} +'''); + } + test('fails if gcloud auth fails', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(exitCode: 1) ]; - createFakePlugin('plugin', packagesDir, extraFiles: [ + + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -67,11 +88,16 @@ void main() { MockProcess(), // auth MockProcess(exitCode: 1), // config ]; - createFakePlugin('plugin', packagesDir, extraFiles: [ + + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, ['firebase-test-lab']); @@ -85,23 +111,29 @@ void main() { }); test('only runs gcloud configuration once', () async { - createFakePlugin('plugin1', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin1 = + createFakePlugin('plugin1', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); - createFakePlugin('plugin2', packagesDir, extraFiles: [ + _writeJavaTestFile(plugin1, javaTestFileRelativePath); + final RepositoryPackage plugin2 = + createFakePlugin('plugin2', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin2, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -142,7 +174,7 @@ void main() { '/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=flame,version=29 --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_firebase_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( @@ -156,7 +188,7 @@ void main() { '/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=flame,version=29 --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_firebase_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'), ]), @@ -164,19 +196,23 @@ void main() { }); test('runs integration tests', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'test/plugin_test.dart', 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/integration_test/should_not_run.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--device', 'model=seoul,version=26', '--test-run-id', @@ -219,7 +255,7 @@ void main() { '/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=flame,version=29 --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_firebase_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( @@ -229,25 +265,95 @@ void main() { '/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=flame,version=29 --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_firebase_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('fails if a test fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + 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_firebase_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_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example/example2'), + ]), + ); + }); + + test('fails if a test fails twice', () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['gcloud'] = [ MockProcess(), // auth MockProcess(), // config MockProcess(exitCode: 1), // integration test #1 + MockProcess(exitCode: 1), // integration test #1 retry MockProcess(), // integration test #2 ]; @@ -257,13 +363,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -282,6 +382,44 @@ void main() { ); }); + test('passes with warning if a test fails once, then passes on retry', + () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + javaTestFileRelativePath, + ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); + + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #1 retry + MockProcess(), // integration test #2 + ]; + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + ]); + + expect( + output, + containsAllInOrder([ + contains('Testing example/integration_test/bar_test.dart...'), + contains('bar_test.dart failed on attempt 1. Retrying...'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + test('fails for packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -294,13 +432,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -321,10 +453,14 @@ void main() { }); test('fails for packages with no integration test files', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); Error? commandError; final List output = await runCapturingPrint( @@ -332,13 +468,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -358,6 +488,48 @@ void main() { ); }); + test('fails for packages with no integration_test runner', () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/should_not_run.dart', + 'example/android/gradlew', + javaTestFileRelativePath, + ]); + // Use the wrong @RunWith annotation. + _writeJavaTestFile(plugin, javaTestFileRelativePath, + runnerClass: 'AndroidJUnit4.class'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No integration_test runner found. ' + 'See the integration_test package README for setup instructions.'), + contains('plugin:\n' + ' No integration_test runner.'), + ]), + ); + }); + test('skips packages with no android directory', () async { createFakePackage('package', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -366,20 +538,14 @@ void main() { final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', + 'model=redfin,version=30', ]); expect( output, containsAllInOrder([ contains('Running for package'), - contains('package/example does not support Android'), + contains('No examples support Android'), ]), ); expect(output, @@ -392,17 +558,19 @@ void main() { }); test('builds if gradlew is missing', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); final List output = await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', - '--device', - 'model=seoul,version=26', + 'model=redfin,version=30', '--test-run-id', 'testRunId', '--build-id', @@ -445,7 +613,7 @@ void main() { '/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=flame,version=29 --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_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), @@ -453,10 +621,14 @@ void main() { }); test('fails if building to generate gradlew fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); processRunner.mockProcessesForExecutable['flutter'] = [ MockProcess(exitCode: 1) // flutter build @@ -468,7 +640,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -484,15 +656,19 @@ void main() { }); test('fails if assembleAndroidTest fails', () async { - final Directory pluginDir = + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + 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] = [ @@ -505,7 +681,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -521,15 +697,19 @@ void main() { }); test('fails if assembleDebug fails', () async { - final Directory pluginDir = + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', - 'example/android/app/src/androidTest/MainActivityTest.java', + 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] = [ @@ -543,7 +723,7 @@ void main() { [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', ], errorHandler: (Error e) { commandError = e; @@ -562,16 +742,20 @@ void main() { }); test('experimental flag', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', 'example/android/gradlew', - 'example/android/app/src/androidTest/MainActivityTest.java', + javaTestFileRelativePath, ]); + _writeJavaTestFile(plugin, javaTestFileRelativePath); await runCapturingPrint(runner, [ 'firebase-test-lab', '--device', - 'model=flame,version=29', + 'model=redfin,version=30', '--test-run-id', 'testRunId', '--build-id', @@ -601,7 +785,7 @@ void main() { '/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=flame,version=29' + '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/example/0/ --device model=redfin,version=30' .split(' '), '/packages/plugin/example'), ]), diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index d278bb2940b8..5bd6f97832f7 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -50,10 +50,10 @@ 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) { + 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(); @@ -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, @@ -101,7 +101,7 @@ void main() { getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -114,7 +114,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 +124,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 +138,7 @@ void main() { getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, formattedFiles) + ..._getPackagesDirRelativePaths(plugin, formattedFiles) ], packagesDir.path), ])); @@ -172,7 +173,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 +191,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -217,7 +218,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 +253,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 +271,7 @@ void main() { '-jar', javaFormatPath, '--replace', - ..._getPackagesDirRelativePaths(pluginDir, files) + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -285,7 +286,7 @@ void main() { 'macos/Classes/Foo.mm', 'windows/foo_plugin.cpp', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -301,8 +302,8 @@ void main() { 'clang-format', [ '-i', - '--style=Google', - ..._getPackagesDirRelativePaths(pluginDir, files) + '--style=file', + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -329,8 +330,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 +339,7 @@ void main() { const List files = [ 'windows/foo_plugin.cpp', ]; - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'a_plugin', packagesDir, extraFiles: files, @@ -357,8 +357,8 @@ void main() { '/path/to/clang-format', [ '-i', - '--style=Google', - ..._getPackagesDirRelativePaths(pluginDir, files) + '--style=file', + ..._getPackagesDirRelativePaths(plugin, files) ], packagesDir.path), ])); @@ -403,7 +403,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: [ @@ -425,15 +425,15 @@ void main() { 'clang-format', [ '-i', - '--style=Google', - ..._getPackagesDirRelativePaths(pluginDir, clangFiles) + '--style=file', + ..._getPackagesDirRelativePaths(plugin, clangFiles) ], packagesDir.path), ProcessCall( getFlutterCommand(mockPlatform), [ 'format', - ..._getPackagesDirRelativePaths(pluginDir, dartFiles) + ..._getPackagesDirRelativePaths(plugin, dartFiles) ], packagesDir.path), ProcessCall( @@ -442,13 +442,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', diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 288cf4696a59..efaf969c83fb 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/plugin_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'); @@ -48,12 +59,14 @@ void main() { 'Use of this source code is governed by a BSD-style license that can be', 'found in the LICENSE file.', ], + bool useCrlf = false, }) { final List lines = ['$prefix$comment$copyright']; for (final String line in license) { lines.add('$comment$line'); } - file.writeAsStringSync(lines.join('\n') + suffix + '\n'); + final String newline = useCrlf ? '\r\n' : '\n'; + file.writeAsStringSync(lines.join(newline) + suffix + newline); } test('looks at only expected extensions', () async { @@ -66,6 +79,7 @@ void main() { 'html': true, 'java': true, 'json': false, + 'kt': true, 'm': true, 'md': false, 'mm': true, @@ -120,6 +134,33 @@ 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(); @@ -139,6 +180,23 @@ void main() { ])); }); + test('passes correct license blocks on Windows', () async { + final File checked = root.childFile('checked.cc'); + checked.createSync(); + _writeLicense(checked, useCrlf: true); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check a file. + expect( + output, + containsAllInOrder([ + contains('Checking checked.cc'), + contains('All files passed validation!'), + ])); + }); + test('handles the comment styles for all supported languages', () async { final File fileA = root.childFile('file_a.cc'); fileA.createSync(); @@ -405,6 +463,24 @@ void main() { ])); }); + test('passes correct LICENSE files on Windows', () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license + .writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n')); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the file. + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('All files passed validation!'), + ])); + }); + test('fails if any first-party LICENSE files are incorrectly formatted', () async { final File license = root.childFile('LICENSE'); @@ -471,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 5670a64f30d8..b072946ff959 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; @@ -40,15 +40,15 @@ void main() { }); test('runs gradle lint', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }); final Directory androidDir = - pluginDir.childDirectory('example').childDirectory('android'); + plugin.getExamples().first.platformDirectory(FlutterPlatform.android); final List output = await runCapturingPrint(runner, ['lint-android']); @@ -72,10 +72,49 @@ 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: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }); Error? commandError; @@ -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: { - kPlatformAndroid: 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:'), ], )); }); @@ -139,7 +186,7 @@ void main() { test('skips non-inline plugins', () async { createFakePlugin('plugin1', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.federated) + platformAndroid: const PlatformDetails(PlatformSupport.federated) }); final List output = diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 44247274028f..516a32fa6925 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -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', @@ -123,48 +123,6 @@ void main() { expect(output, contains('Bar')); }); - test('allow warnings for podspecs with known warnings', () async { - final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - final List output = await runCapturingPrint( - runner, ['podspecs', '--ignore-warnings=plugin1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', const ['pod'], packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin1Dir.childFile('plugin1.podspec').path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--allow-warnings', - '--use-libraries' - ], - packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin1Dir.childFile('plugin1.podspec').path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--allow-warnings', - ], - packagesDir.path), - ]), - ); - - expect(output, contains('Linting plugin1.podspec')); - }); - test('fails if pod is missing', () async { createFakePlugin('plugin1', packagesDir, extraFiles: ['plugin1.podspec']); diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart index fcdf9fafdb63..f74431c5cee7 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; @@ -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 new file mode 100644 index 000000000000..2644e814f578 --- /dev/null +++ b/script/tool/test/make_deps_path_based_command_test.dart @@ -0,0 +1,405 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + processRunner = RecordingProcessRunner(); + final MakeDepsPathBasedCommand command = + MakeDepsPathBasedCommand(packagesDir, gitDir: gitDir); + + runner = CommandRunner( + 'make-deps-path-based_command', 'Test for $MakeDepsPathBasedCommand'); + runner.addCommand(command); + }); + + /// Adds dummy 'dependencies:' entries for each package in [dependencies] + /// to [package]. + void _addDependencies( + RepositoryPackage package, Iterable dependencies) { + final List lines = package.pubspecFile.readAsLinesSync(); + final int dependenciesStartIndex = lines.indexOf('dependencies:'); + assert(dependenciesStartIndex != -1); + lines.insertAll(dependenciesStartIndex + 1, [ + for (final String dependency in dependencies) ' $dependency: ^1.0.0', + ]); + package.pubspecFile.writeAsStringSync(lines.join('\n')); + } + + test('no-ops for no plugins', () async { + createFakePackage('foo', packagesDir, isFlutter: true); + final RepositoryPackage packageBar = + createFakePackage('bar', packagesDir, isFlutter: true); + _addDependencies(packageBar, ['foo']); + final String originalPubspecContents = + packageBar.pubspecFile.readAsStringSync(); + + final List output = + await runCapturingPrint(runner, ['make-deps-path-based']); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + // The 'foo' reference should not have been modified. + expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); + }); + + test('rewrites references', () async { + final RepositoryPackage simplePackage = + createFakePackage('foo', packagesDir, isFlutter: true); + final Directory pluginGroup = packagesDir.childDirectory('bar'); + + createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); + final RepositoryPackage pluginImplementation = + createFakePlugin('bar_android', pluginGroup); + final RepositoryPackage pluginAppFacing = + createFakePlugin('bar', pluginGroup); + + _addDependencies(simplePackage, [ + 'bar', + 'bar_android', + 'bar_platform_interface', + ]); + _addDependencies(pluginAppFacing, [ + 'bar_platform_interface', + 'bar_android', + ]); + _addDependencies(pluginImplementation, [ + 'bar_platform_interface', + ]); + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + + expect( + output, + containsAll([ + 'Rewriting references to: bar, bar_platform_interface...', + ' Modified packages/bar/bar/pubspec.yaml', + ' Modified packages/bar/bar_android/pubspec.yaml', + ' Modified packages/foo/pubspec.yaml', + ])); + expect( + output, + isNot(contains( + ' Modified packages/bar/bar_platform_interface/pubspec.yaml'))); + + expect( + simplePackage.pubspecFile.readAsLinesSync(), + containsAllInOrder([ + '# FOR TESTING ONLY. DO NOT MERGE.', + 'dependency_overrides:', + ' bar:', + ' path: ../bar/bar', + ' bar_platform_interface:', + ' path: ../bar/bar_platform_interface', + ])); + expect( + pluginAppFacing.pubspecFile.readAsLinesSync(), + containsAllInOrder([ + 'dependency_overrides:', + ' bar_platform_interface:', + ' path: ../../bar/bar_platform_interface', + ])); + }); + + // This test case ensures that running CI using this command on an interim + // PR that itself used this command won't fail on the rewrite step. + test('running a second time no-ops without failing', () async { + final RepositoryPackage simplePackage = + createFakePackage('foo', packagesDir, isFlutter: true); + final Directory pluginGroup = packagesDir.childDirectory('bar'); + + createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); + final RepositoryPackage pluginImplementation = + createFakePlugin('bar_android', pluginGroup); + final RepositoryPackage pluginAppFacing = + createFakePlugin('bar', pluginGroup); + + _addDependencies(simplePackage, [ + 'bar', + 'bar_android', + 'bar_platform_interface', + ]); + _addDependencies(pluginAppFacing, [ + 'bar_platform_interface', + 'bar_android', + ]); + _addDependencies(pluginImplementation, [ + 'bar_platform_interface', + ]); + + await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies=bar,bar_platform_interface' + ]); + + expect( + output, + containsAll([ + 'Rewriting references to: bar, bar_platform_interface...', + ' Skipped packages/bar/bar/pubspec.yaml - Already rewritten', + ' Skipped packages/bar/bar_android/pubspec.yaml - Already rewritten', + ' Skipped packages/foo/pubspec.yaml - Already rewritten', + ])); + }); + + group('target-dependencies-with-non-breaking-updates', () { + test('no-ops for no published changes', () async { + final RepositoryPackage package = createFakePackage('foo', packagesDir); + + final String changedFileOutput = [ + package.pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: package.pubspecFile.readAsStringSync()), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + }); + + test('no-ops for no deleted packages', () async { + final String changedFileOutput = [ + // A change for a file that's not on disk simulates a deletion. + packagesDir.childDirectory('foo').childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Skipping foo; deleted.'), + contains('No target dependencies'), + ]), + ); + }); + + test('includes bugfix version changes as targets', () async { + const String newVersion = '1.0.1'; + final RepositoryPackage package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = package.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Rewriting references to: foo...'), + ]), + ); + }); + + test('includes minor version changes to 1.0+ as targets', () async { + const String newVersion = '1.1.0'; + final RepositoryPackage package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = package.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('Rewriting references to: foo...'), + ]), + ); + }); + + test('does not include major version changes as targets', () async { + const String newVersion = '2.0.0'; + final RepositoryPackage package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = package.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + }); + + test('does not include minor version changes to 0.x as targets', () async { + const String newVersion = '0.8.0'; + final RepositoryPackage package = + createFakePackage('foo', packagesDir, version: newVersion); + + final File pubspecFile = package.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '0.7.0'); + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains('No target dependencies'), + ]), + ); + }); + + test('skips anything outside of the packages directory', () async { + final Directory toolDir = packagesDir.parent.childDirectory('tool'); + const String newVersion = '1.1.0'; + final RepositoryPackage package = createFakePackage( + 'flutter_plugin_tools', toolDir, + version: newVersion); + + // Simulate a minor version change so it would be a target. + final File pubspecFile = package.pubspecFile; + final String changedFileOutput = [ + pubspecFile, + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + final String gitPubspecContents = + pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: gitPubspecContents), + ]; + + final List output = await runCapturingPrint(runner, [ + 'make-deps-path-based', + '--target-dependencies-with-non-breaking-updates' + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'Skipping /tool/flutter_plugin_tools/pubspec.yaml; not in packages directory.'), + contains('No target dependencies'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 3d0aef1b3971..f6333ebd367d 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -30,6 +30,9 @@ class MockPlatform extends Mock implements Platform { Uri get script => isWindows ? Uri.file(r'C:\foo\bar', windows: true) : Uri.file('/foo/bar', windows: false); + + @override + Map environment = {}; } class MockProcess extends Mock implements io.Process { diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index ba93efcb3ace..d420184b6125 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -8,10 +8,12 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/cmake.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/file_utils.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/native_test_command.dart'; +import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -53,6 +55,16 @@ final Map _kDeviceListMap = { } }; +const String _fakeCmakeCommand = 'path/to/cmake'; + +void _createFakeCMakeCache(RepositoryPackage plugin, Platform platform) { + final CMakeProject project = CMakeProject(getExampleDir(plugin), + platform: platform, buildMode: 'Release'); + final File cache = project.buildDirectory.childFile('CMakeCache.txt'); + cache.createSync(recursive: true); + cache.writeAsStringSync('CMAKE_COMMAND:INTERNAL=$_fakeCmakeCommand'); +} + // TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of // doing all the process mocking and validation. void main() { @@ -67,7 +79,10 @@ void main() { setUp(() { fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); + // iOS and macOS tests expect macOS, Linux tests expect Linux; nothing + // needs to distinguish between Linux and macOS, so set both to true to + // allow them to share a setup group. + mockPlatform = MockPlatform(isMacOS: true, isLinux: true); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); final NativeTestCommand command = NativeTestCommand(packagesDir, @@ -133,6 +148,25 @@ void main() { package.path); } + // Returns the ProcessCall to expect for build the Linux unit tests for the + // given plugin. + ProcessCall _getLinuxBuildCall(RepositoryPackage plugin) { + return ProcessCall( + 'cmake', + [ + '--build', + getExampleDir(plugin) + .childDirectory('build') + .childDirectory('linux') + .childDirectory('x64') + .childDirectory('release') + .path, + '--target', + 'unit_tests' + ], + null); + } + test('fails if no platforms are provided', () async { Error? commandError; final List output = await runCapturingPrint( @@ -170,13 +204,12 @@ void main() { }); test('reports skips with no tests', () async { - final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), @@ -206,7 +239,7 @@ void main() { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint(runner, @@ -223,7 +256,7 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.federated) + platformIOS: const PlatformDetails(PlatformSupport.federated) }); final List output = await runCapturingPrint(runner, @@ -238,13 +271,12 @@ void main() { }); test('running with correct destination', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: 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( @@ -276,12 +308,11 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: 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 @@ -331,7 +362,7 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + platformMacOS: const PlatformDetails(PlatformSupport.federated), }); final List output = @@ -347,14 +378,12 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -382,11 +411,11 @@ void main() { group('Android', () { test('runs Java unit tests in Android implementation folder', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -396,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, @@ -412,11 +443,11 @@ void main() { }); test('runs Java unit tests in example folder', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -426,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, @@ -442,11 +475,11 @@ void main() { }); test('runs Java integration tests', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -457,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, @@ -482,7 +517,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -504,11 +539,11 @@ void main() { }); test('runs all tests when present', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -519,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, @@ -543,11 +580,11 @@ void main() { }); test('honors --no-unit', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -559,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, @@ -578,11 +617,11 @@ void main() { }); test('honors --no-integration', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -594,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, @@ -614,7 +655,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/app/src/test/example_test.java', @@ -646,7 +687,7 @@ void main() { 'plugin1', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -658,7 +699,7 @@ void main() { 'plugin2', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -685,11 +726,11 @@ void main() { }); test('fails when a unit test fails', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -697,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] = [ @@ -726,11 +768,11 @@ void main() { }); test('fails when an integration test fails', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -739,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] = [ @@ -773,7 +816,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -826,7 +869,7 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + platformAndroid: const PlatformDetails(PlatformSupport.inline) }, ); @@ -844,17 +887,18 @@ void main() { }); group('Linux', () { - test('runs unit tests', () async { + test('builds and runs unit tests', () async { const String testBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; - final Directory pluginDirectory = + 'build/linux/x64/release/bar/plugin_test'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -874,25 +918,27 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); test('only runs release unit tests', () async { const String debugTestBinaryRelativePath = - 'build/linux/foo/debug/bar/plugin_test'; + 'build/linux/x64/debug/bar/plugin_test'; const String releaseTestBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; - final Directory pluginDirectory = + 'build/linux/x64/release/bar/plugin_test'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(plugin, mockPlatform); final File releaseTestBinary = childFileWithSubcomponents( - pluginDirectory, + plugin.directory, ['example', ...releaseTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -909,18 +955,18 @@ void main() { ]), ); - // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(plugin), ProcessCall(releaseTestBinary.path, const [], null), ])); }); - test('fails if there are no unit tests', () async { + test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); Error? commandError; @@ -936,24 +982,57 @@ void main() { expect( output, containsAllInOrder([ - contains('No test binaries found.'), + contains('plugin:\n' + ' Examples must be built before testing.') ]), ); expect(processRunner.recordedCalls, orderedEquals([])); }); + test('fails if there are no unit tests', () async { + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformLinux: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getLinuxBuildCall(plugin), + ])); + }); + test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/linux/foo/release/bar/plugin_test'; - final Directory pluginDirectory = + 'build/linux/x64/release/bar/plugin_test'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + platformLinux: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); processRunner.mockProcessesForExecutable[testBinary.path] = @@ -979,6 +1058,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getLinuxBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); @@ -989,7 +1069,7 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -1014,14 +1094,12 @@ void main() { }); test('honors unit-only', () async { - final Directory pluginDirectory1 = createFakePlugin( - 'plugin', packagesDir, + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1050,14 +1128,13 @@ void main() { }); test('honors integration-only', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1086,14 +1163,13 @@ void main() { }); test('skips when the requested target is not present', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + 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'] = [ @@ -1122,14 +1198,13 @@ void main() { }); test('fails if there are no unit tests', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess(['RunnerUITests']), @@ -1162,14 +1237,13 @@ void main() { }); test('fails if unable to check for requested target', () async { - final Directory pluginDirectory1 = createFakePlugin( + final RepositoryPackage plugin1 = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin1); processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess(exitCode: 1), // xcodebuild -list @@ -1202,7 +1276,7 @@ void main() { group('multiplatform', () { test('runs all platfroms when supported', () async { - final Directory pluginDirectory = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -1210,14 +1284,13 @@ void main() { 'android/src/test/example_test.java', ], platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }, ); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); final Directory androidFolder = pluginExampleDirectory.childDirectory('android'); @@ -1261,14 +1334,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: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); - final Directory pluginExampleDirectory = - pluginDirectory1.childDirectory('example'); + final Directory pluginExampleDirectory = getExampleDir(plugin); processRunner.mockProcessesForExecutable['xcrun'] = [ _getMockXcodebuildListProcess( @@ -1299,13 +1370,12 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: 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( @@ -1366,9 +1436,9 @@ void main() { 'plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + platformMacOS: const PlatformDetails(PlatformSupport.inline, hasDartCode: true, hasNativeCode: false), - kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + platformWindows: const PlatformDetails(PlatformSupport.inline, hasDartCode: true, hasNativeCode: false), }, ); @@ -1393,12 +1463,12 @@ void main() { }); test('failing one platform does not stop the tests', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1412,9 +1482,10 @@ void main() { ]; // 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] = [ @@ -1449,12 +1520,12 @@ void main() { }); test('failing multiple platforms reports multiple failures', () async { - final Directory pluginDir = createFakePlugin( + final RepositoryPackage plugin = createFakePlugin( 'plugin', packagesDir, platformSupport: { - kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), - kPlatformIos: const PlatformDetails(PlatformSupport.inline), + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1463,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] = [ @@ -1524,18 +1596,38 @@ void main() { runner.addCommand(command); }); + // Returns the ProcessCall to expect for build the Windows unit tests for + // the given plugin. + ProcessCall _getWindowsBuildCall(RepositoryPackage plugin) { + return ProcessCall( + _fakeCmakeCommand, + [ + '--build', + getExampleDir(plugin) + .childDirectory('build') + .childDirectory('windows') + .path, + '--target', + 'unit_tests', + '--config', + 'Debug' + ], + null); + } + group('Windows', () { test('runs unit tests', () async { const String testBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; - final Directory pluginDirectory = + 'build/windows/Debug/bar/plugin_test.exe'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ @@ -1555,26 +1647,28 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _getWindowsBuildCall(plugin), ProcessCall(testBinary.path, const [], null), ])); }); - test('only runs release unit tests', () async { + test('only runs debug unit tests', () async { const String debugTestBinaryRelativePath = - 'build/windows/foo/Debug/bar/plugin_test.exe'; + 'build/windows/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; - final Directory pluginDirectory = + 'build/windows/Release/bar/plugin_test.exe'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$debugTestBinaryRelativePath', 'example/$releaseTestBinaryRelativePath' ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(plugin, mockPlatform); - final File releaseTestBinary = childFileWithSubcomponents( - pluginDirectory, - ['example', ...releaseTestBinaryRelativePath.split('/')]); + final File debugTestBinary = childFileWithSubcomponents( + plugin.directory, + ['example', ...debugTestBinaryRelativePath.split('/')]); final List output = await runCapturingPrint(runner, [ 'native-test', @@ -1590,18 +1684,18 @@ void main() { ]), ); - // Only the release version should be run. expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall(releaseTestBinary.path, const [], null), + _getWindowsBuildCall(plugin), + ProcessCall(debugTestBinary.path, const [], null), ])); }); - test('fails if there are no unit tests', () async { + test('fails if CMake has not been configured', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); Error? commandError; @@ -1617,24 +1711,57 @@ void main() { expect( output, containsAllInOrder([ - contains('No test binaries found.'), + contains('plugin:\n' + ' Examples must be built before testing.') ]), ); expect(processRunner.recordedCalls, orderedEquals([])); }); + test('fails if there are no unit tests', () async { + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, + platformSupport: { + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getWindowsBuildCall(plugin), + ])); + }); + test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/windows/foo/Release/bar/plugin_test.exe'; - final Directory pluginDirectory = + 'build/windows/Debug/bar/plugin_test.exe'; + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { - kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + platformWindows: const PlatformDetails(PlatformSupport.inline), }); + _createFakeCMakeCache(plugin, mockPlatform); - final File testBinary = childFileWithSubcomponents(pluginDirectory, + final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); processRunner.mockProcessesForExecutable[testBinary.path] = @@ -1660,6 +1787,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + _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..e6c5b9cdebc5 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -44,9 +44,9 @@ void main() { }); test('publish check all packages', () async { - final Directory plugin1Dir = + final RepositoryPackage plugin1 = createFakePlugin('plugin_tools_test_package_a', packagesDir); - final Directory plugin2Dir = + final RepositoryPackage plugin2 = createFakePlugin('plugin_tools_test_package_b', packagesDir); await runCapturingPrint(runner, ['publish-check']); @@ -57,11 +57,11 @@ void main() { ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], - plugin1Dir.path), + plugin1.path), ProcessCall( 'flutter', const ['pub', 'publish', '--', '--dry-run'], - plugin2Dir.path), + plugin2.path), ])); }); @@ -89,8 +89,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 +108,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 +129,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 +373,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_plugin_command_test.dart index 14e99a10f365..f3be3b48b1f1 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -83,11 +83,11 @@ 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; @@ -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'] = [ @@ -183,7 +183,7 @@ void main() { }); test('forwards --pub-publish-flags to pub publish', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ @@ -198,14 +198,14 @@ 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 = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ @@ -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-plugin', + '--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 { @@ -249,7 +278,7 @@ void main() { }); test('publish, dry run', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); final List output = @@ -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!'), @@ -386,7 +415,7 @@ void main() { }); test('to upstream by default, dry run', () async { - final Directory pluginDir = + final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, examples: []); mockStdin.readLineOutput = 'y'; @@ -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!'), @@ -447,16 +476,17 @@ 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'; @@ -468,8 +498,8 @@ void main() { 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,8 +546,8 @@ 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'; @@ -527,8 +558,8 @@ void main() { 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,15 +583,16 @@ 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'; @@ -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,17 +635,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') ]; mockStdin.readLineOutput = 'y'; @@ -623,9 +655,9 @@ void main() { 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,17 +684,17 @@ 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'; @@ -672,10 +704,10 @@ void main() { 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 at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n'), + 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n'), contains('SKIPPING: package deleted'), contains('skipped (with warning)'), ])); @@ -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( @@ -748,17 +780,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') ]; Error? commandError; @@ -785,15 +817,16 @@ 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, @@ -812,10 +845,10 @@ 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, @@ -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 c5d36013c40b..2c254ca94984 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -12,116 +12,162 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; -void main() { - group('test pubspec_check_command', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - /// Returns the top section of a pubspec.yaml for a package named [name], - /// for either a flutter/packages or flutter/plugins package depending on - /// the values of [isPlugin]. - /// - /// By default it will create a header that includes all of the expected - /// values, elements can be changed via arguments to create incorrect - /// entries. - /// - /// If [includeRepository] is true, by default the path in the link will - /// be "packages/[name]"; a different "packages"-relative path can be - /// provided with [repositoryPackagesDirRelativePath]. - String headerSection( - String name, { - bool isPlugin = false, - bool includeRepository = true, - String? repositoryPackagesDirRelativePath, - bool includeHomepage = false, - bool includeIssueTracker = true, - bool publishable = true, - }) { - final String repositoryPath = repositoryPackagesDirRelativePath ?? name; - final String repoLink = 'https://github.com/flutter/' - '${isPlugin ? 'plugins' : 'packages'}/tree/master/' - 'packages/$repositoryPath'; - final String issueTrackerLink = - 'https://github.com/flutter/flutter/issues?' - 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; - return ''' +/// Returns the top section of a pubspec.yaml for a package named [name], +/// for either a flutter/packages or flutter/plugins package depending on +/// the values of [isPlugin]. +/// +/// By default it will create a header that includes all of the expected +/// values, elements can be changed via arguments to create incorrect +/// entries. +/// +/// If [includeRepository] is true, by default the path in the link will +/// be "packages/[name]"; a different "packages"-relative path can be +/// provided with [repositoryPackagesDirRelativePath]. +String _headerSection( + String name, { + bool isPlugin = false, + bool includeRepository = true, + String repositoryBranch = 'main', + String? repositoryPackagesDirRelativePath, + bool includeHomepage = false, + bool includeIssueTracker = true, + bool publishable = true, + String? description, +}) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; + final List repoLinkPathComponents = [ + 'flutter', + if (isPlugin) 'plugins' else 'packages', + 'tree', + repositoryBranch, + 'packages', + repositoryPath, + ]; + final String repoLink = + 'https://github.com/${repoLinkPathComponents.join('/')}'; + final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' + 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + description ??= 'A test package for validating that the pubspec.yaml ' + 'follows repo best practices.'; + return ''' name: $name +description: $description ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 -${publishable ? '' : 'publish_to: \'none\''} +${publishable ? '' : "publish_to: 'none'"} '''; - } +} - String environmentSection() { - return ''' +String _environmentSection() { + return ''' environment: sdk: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" '''; - } +} - String flutterSection({ - bool isPlugin = false, - String? implementedPackage, - }) { - final String pluginEntry = ''' +String _flutterSection({ + bool isPlugin = false, + String? implementedPackage, + Map> pluginPlatformDetails = + const >{}, +}) { + String pluginEntry = ''' plugin: ${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; - return ''' + + for (final MapEntry> platform + in pluginPlatformDetails.entries) { + pluginEntry += ''' + ${platform.key}: +'''; + for (final MapEntry detail in platform.value.entries) { + pluginEntry += ''' + ${detail.key}: ${detail.value} +'''; + } + } + + return ''' flutter: ${isPlugin ? pluginEntry : ''} '''; - } +} - String dependenciesSection() { - return ''' +String _dependenciesSection() { + return ''' dependencies: flutter: sdk: flutter '''; - } +} - String devDependenciesSection() { - return ''' +String _devDependenciesSection() { + return ''' dev_dependencies: flutter_test: sdk: flutter '''; - } +} + +String _falseSecretsSection() { + return ''' +false_secrets: + - /lib/main.dart +'''; +} + +void main() { + group('test pubspec_check_command', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); test('passes for a plugin following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_falseSecretsSection()} +'''); + + plugin.getExamples().first.pubspecFile.writeAsStringSync(''' +${_headerSection( + 'plugin_example', + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${_environmentSection()} +${_dependenciesSection()} +${_flutterSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -139,14 +185,28 @@ ${devDependenciesSection()} }); test('passes for a Flutter package following conventions', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin')} -${environmentSection()} -${dependenciesSection()} -${devDependenciesSection()} -${flutterSection()} + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('a_package')} +${_environmentSection()} +${_dependenciesSection()} +${_devDependenciesSection()} +${_flutterSection()} +${_falseSecretsSection()} +'''); + + package.getExamples().first.pubspecFile.writeAsStringSync(''' +${_headerSection( + 'a_package', + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${_environmentSection()} +${_dependenciesSection()} +${_flutterSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -156,21 +216,21 @@ ${flutterSection()} 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(''' -${headerSection('package')} -${environmentSection()} -${dependenciesSection()} + package.pubspecFile.writeAsStringSync(''' +${_headerSection('package')} +${_environmentSection()} +${_dependenciesSection()} '''); final List output = await runCapturingPrint(runner, [ @@ -187,14 +247,15 @@ ${dependenciesSection()} }); test('fails when homepage is included', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeHomepage: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, includeHomepage: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -214,14 +275,15 @@ ${devDependenciesSection()} }); test('fails when repository is missing', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeRepository: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, includeRepository: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -240,14 +302,15 @@ ${devDependenciesSection()} }); test('fails when homepage is given instead of repository', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -266,15 +329,16 @@ ${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(''' -${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -292,15 +356,43 @@ ${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); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -318,15 +410,108 @@ ${devDependenciesSection()} ); }); + test('fails when description is too short', () async { + final RepositoryPackage plugin = createFakePlugin( + 'a_plugin', packagesDir.childDirectory('a_plugin'), + examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, description: 'Too short')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test( + 'allows short descriptions for non-app-facing parts of federated plugins', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, description: 'Too short')} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test('fails when description is too long', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + const String description = 'This description is too long. It just goes ' + 'on and on and on and on and on. pub.dev will down-score it because ' + 'there is just too much here. Someone shoul really cut this down to just ' + 'the core description so that search results are more useful and the ' + 'package does not lose pub points.'; + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true, description: description)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + test('fails when environment section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${environmentSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true)} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_environmentSection()} '''); Error? commandError; @@ -346,14 +531,15 @@ ${environmentSection()} }); test('fails when flutter section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${flutterSection(isPlugin: true)} -${environmentSection()} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true)} +${_flutterSection(isPlugin: true)} +${_environmentSection()} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -373,14 +559,15 @@ ${devDependenciesSection()} }); test('fails when dependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${devDependenciesSection()} -${dependenciesSection()} + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_devDependenciesSection()} +${_dependenciesSection()} '''); Error? commandError; @@ -399,15 +586,44 @@ ${dependenciesSection()} ); }); - test('fails when devDependencies section is out of order', () async { - final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + test('fails when dev_dependencies section is out of order', () async { + final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin', isPlugin: true)} -${environmentSection()} -${devDependenciesSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_devDependenciesSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + + test('fails when false_secrets section is out of order', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_falseSecretsSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -428,15 +644,16 @@ ${dependenciesSection()} test('fails when an implemenation package is missing "implements"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin_a_foo', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -456,15 +673,16 @@ ${devDependenciesSection()} test('fails when an implemenation package has the wrong "implements"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); - - pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} -${environmentSection()} -${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} -${dependenciesSection()} -${devDependenciesSection()} + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), + examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin_a_foo', isPlugin: true)} +${_environmentSection()} +${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${_dependenciesSection()} +${_devDependenciesSection()} '''); Error? commandError; @@ -484,19 +702,20 @@ ${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(''' -${headerSection( + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection( 'plugin_a_foo', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', )} -${environmentSection()} -${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -511,20 +730,99 @@ ${devDependenciesSection()} ); }); + test('fails when a "default_package" looks incorrect', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} +${_environmentSection()} +${_flutterSection( + isPlugin: true, + pluginPlatformDetails: >{ + 'android': {'default_package': 'plugin_b_android'} + }, + )} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + '"plugin_b_android" is not an expected implementation name for "plugin_a"'), + ]), + ); + }); + + test( + 'fails when a "default_package" does not have a corresponding dependency', + () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin_a', packagesDir.childDirectory('plugin_a'), + examples: []); + + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} +${_environmentSection()} +${_flutterSection( + isPlugin: true, + pluginPlatformDetails: >{ + 'android': {'default_package': 'plugin_a_android'} + }, + )} +${_dependenciesSection()} +${_devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following default_packages are missing corresponding ' + 'dependencies:\n plugin_a_android'), + ]), + ); + }); + test('passes for an app-facing package without "implements"', () async { - final 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(''' -${headerSection( + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection( 'plugin_a', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -541,21 +839,21 @@ ${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(''' -${headerSection( + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection( 'plugin_a_platform_interface', isPlugin: true, repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_platform_interface', )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -571,17 +869,18 @@ ${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(''' -${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} -${environmentSection()} + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} +${_environmentSection()} '''); Error? commandError; @@ -602,22 +901,23 @@ ${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(''' -${headerSection( + plugin.pubspecFile.writeAsStringSync(''' +${_headerSection( 'plugin', isPlugin: true, publishable: false, includeRepository: false, includeIssueTracker: false, )} -${environmentSection()} -${flutterSection(isPlugin: true)} -${dependenciesSection()} -${devDependenciesSection()} +${_environmentSection()} +${_flutterSection(isPlugin: true)} +${_dependenciesSection()} +${_devDependenciesSection()} '''); final List output = @@ -632,4 +932,51 @@ ${devDependenciesSection()} ); }); }); + + group('test pubspec_check_command on Windows', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); + + test('repository check works', () async { + final RepositoryPackage package = + createFakePackage('package', packagesDir, examples: []); + + package.pubspecFile.writeAsStringSync(''' +${_headerSection('package')} +${_environmentSection()} +${_dependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for package...'), + contains('No issues found!'), + ]), + ); + }); + }); } diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart new file mode 100644 index 000000000000..fa4fc604dd73 --- /dev/null +++ b/script/tool/test/readme_check_command_test.dart @@ -0,0 +1,560 @@ +// Copyright 2013 The Flutter Authors. 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 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); + + 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 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.'), + contains('Missing code-excerpt management for code block'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index f8aca38d3478..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,12 +218,12 @@ 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'], platformSupport: { - kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + platformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -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..5c1d74444eea --- /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/plugin_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: ['example/build.excerpt.yaml']); + 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: ['example/build.excerpt.yaml']); + 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: ['example/build.excerpt.yaml']); + + 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: ['example/build.excerpt.yaml']); + + 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: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // 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: ['example/build.excerpt.yaml']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 0), // dart pub get + MockProcess(exitCode: 0), // 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: ['example/build.excerpt.yaml']); + + 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: ['example/build.excerpt.yaml']); + + 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..7e7ff54d5947 --- /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/plugin_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 9abb34bef35a..041d93367f9f 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,11 @@ import 'package:quiver/collection.dart'; import 'mocks.dart'; +export 'package:flutter_plugin_tools/src/common/repository_package.dart'; + +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 +53,6 @@ Directory createPackagesDirectory( class PlatformDetails { const PlatformDetails( this.type, { - this.variants = const [], this.hasNativeCode = true, this.hasDartCode = false, }); @@ -55,9 +60,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 +71,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 +92,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 +100,180 @@ 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; -} - -void createFakeCHANGELOG(Directory parent, String texts) { - parent.childFile('CHANGELOG.md').createSync(); - parent.childFile('CHANGELOG.md').writeAsStringSync(texts); + return package; } -/// 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( @@ -229,24 +290,24 @@ String _pluginPlatformSection( ' $platform:', ]; switch (platform) { - case kPlatformAndroid: + case platformAndroid: lines.add(' package: io.flutter.plugins.fake'); continue nativeByDefault; nativeByDefault: - case kPlatformIos: - case kPlatformLinux: - case kPlatformMacos: - case kPlatformWindows: + case platformIOS: + case platformLinux: + case platformMacOS: + case platformWindows: if (support.hasNativeCode) { final String className = - platform == kPlatformIos ? 'FLTFakePlugin' : 'FakePlugin'; + platform == platformIOS ? 'FLTFakePlugin' : 'FakePlugin'; lines.add(' pluginClass: $className'); } if (support.hasDartCode) { lines.add(' dartPluginClass: FakeDartPlugin'); } break; - case kPlatformWeb: + case platformWeb: lines.addAll([ ' pluginClass: FakePlugin', ' fileName: ${packageName}_web.dart', @@ -256,32 +317,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 +347,11 @@ Future> runCapturingPrint( rethrow; } errorHandler(e); + } on Exception catch (e) { + if (exceptionHandler == null) { + rethrow; + } + exceptionHandler(e); } return prints; @@ -400,8 +455,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 9ab7c57089a3..8f8d510fd106 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_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 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -22,15 +21,15 @@ import 'mocks.dart'; import 'util.dart'; void testAllowedVersion( - String masterVersion, + String mainVersion, String headVersion, { bool allowed = true, NextVersionType? nextVersionType, }) { - final Version master = Version.parse(masterVersion); + final Version main = Version.parse(mainVersion); final Version head = Version.parse(headVersion); final Map allowedVersions = - getAllowedNextVersions(master, newVersion: head); + getAllowedNextVersions(main, newVersion: head); if (allowed) { expect(allowedVersions, contains(head)); if (nextVersionType != null) { @@ -45,14 +44,12 @@ class MockProcessResult extends Mock implements io.ProcessResult {} void main() { const String indentation = ' '; - group('$VersionCheckCommand', () { - FileSystem fileSystem; + group('VersionCheckCommand', () { + late FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; - late List> gitDirCommands; - Map gitShowResponses; late MockGitDir gitDir; // Ignored if mockHttpResponse is set. int mockHttpStatus; @@ -63,27 +60,17 @@ void main() { mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - gitDirCommands = >[]; - gitShowResponses = {}; gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'show') { - final String? response = - gitShowResponses[invocation.positionalArguments[0][1]]; - if (response == null) { - throw const io.ProcessException('git', ['show']); - } - when(mockProcessResult.stdout as String?) - .thenReturn(response); - } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String?) - .thenReturn('abc123'); - } - return Future.value(mockProcessResult); + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); }); // Default to simulating the plugin never having been published. @@ -108,11 +95,11 @@ void main() { test('allows valid version', () async { createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, @@ -121,39 +108,49 @@ void main() { contains('1.0.0 -> 2.0.0'), ]), ); - expect(gitDirCommands.length, equals(1)); expect( - gitDirCommands, - containsAll([ - equals(['show', 'master:packages/plugin/pubspec.yaml']), + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) ])); }); test('denies invalid version', () async { createFakePlugin('plugin', packagesDir, version: '0.2.0'); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', - }; - final Future> result = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + 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; + }); - await expectLater( - result, - throwsA(isA()), - ); - expect(gitDirCommands.length, equals(1)); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); expect( - gitDirCommands, - containsAll([ - equals(['show', 'master:packages/plugin/pubspec.yaml']), + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) ])); }); - test('allows valid version without explicit base-sha', () async { + test('uses merge-base without explicit base-sha', () async { createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-merge-base'] = [ + MockProcess(stdout: 'abc123'), + MockProcess(stdout: 'abc123'), + ]; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint(runner, ['version-check']); @@ -164,6 +161,14 @@ void main() { contains('1.0.0 -> 2.0.0'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall('git-merge-base', + ['--fork-point', 'FETCH_HEAD', 'HEAD'], null), + ProcessCall('git-show', + ['abc123:packages/plugin/pubspec.yaml'], null), + ])); }); test('allows valid version for new package.', () async { @@ -182,11 +187,11 @@ void main() { test('allows likely reverts.', () async { createFakePlugin('plugin', packagesDir, version: '0.6.1'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - }; - final List output = - await runCapturingPrint(runner, ['version-check']); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.6.2'), + ]; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main']); expect( output, @@ -195,45 +200,49 @@ void main() { 'This is assumed to be a revert.'), ]), ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) + ])); }); test('denies lower version that could not be a simple revert', () async { createFakePlugin('plugin', packagesDir, version: '0.5.1'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', - }; - final Future> result = - runCapturingPrint(runner, ['version-check']); - - await expectLater( - result, - throwsA(isA()), - ); - }); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 0.6.2'), + ]; - test('denies invalid version without explicit base-sha', () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0'); - gitShowResponses = { - 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', - }; - final Future> result = - runCapturingPrint(runner, ['version-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), - ); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Incorrectly updated version.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', ['main:packages/plugin/pubspec.yaml'], null) + ])); }); test('allows minor changes to platform interfaces', () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '1.1.0'); - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -241,54 +250,180 @@ void main() { contains('1.0.0 -> 1.1.0'), ]), ); - expect(gitDirCommands.length, equals(1)); - expect( - gitDirCommands, - containsAll([ - equals([ - 'show', - 'master:packages/plugin_platform_interface/pubspec.yaml' - ]), + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) ])); }); - test('disallows breaking changes to platform interfaces', () async { + test('disallows breaking changes to platform interfaces by default', + () async { createFakePlugin('plugin_platform_interface', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'master:packages/plugin_platform_interface/pubspec.yaml': - 'version: 1.0.0', - }; - final Future> output = runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - await expectLater( + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + ' Breaking changes to platform interfaces are not allowed ' + 'without explicit justification.\n' + ' See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' + 'for more information.'), + ])); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) + ])); + }); + + test('allows breaking changes to platform interfaces with explanation', + () 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}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface ' + 'due to "## Breaking change justification" in the change ' + 'description.'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) + ])); + }); + + test('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, - throwsA(isA()), + containsAllInOrder([ + contains('No such file: a_missing_file.txt'), + ]), ); - expect(gitDirCommands.length, equals(1)); - expect( - gitDirCommands, - containsAll([ - equals([ - 'show', - 'master:packages/plugin_platform_interface/pubspec.yaml' - ]), + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) + ])); + }); + + test('allows breaking changes to platform interfaces with bypass flag', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=main', + '--ignore-platform-interface-breaks' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface due ' + 'to --ignore-platform-interface-breaks'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + expect( + processRunner.recordedCalls, + containsAllInOrder(const [ + ProcessCall( + 'git-show', + [ + 'main:packages/plugin_platform_interface/pubspec.yaml' + ], + null) ])); }); test('Allow empty lines in front of the first version in CHANGELOG', () async { const String version = '1.0.1'; - final 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=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -298,24 +433,21 @@ void main() { }); 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); - bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { - expect(e, isA()); - hasError = true; + plugin.changelogFile.writeAsStringSync(changelog); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { + commandError = e; }); - expect(hasError, isTrue); + expect(commandError, isA()); expect( output, containsAllInOrder([ @@ -326,16 +458,16 @@ void main() { 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=master']); + runner, ['version-check', '--base-sha=main']); expect( output, containsAllInOrder([ @@ -347,7 +479,7 @@ void main() { 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 = ''' @@ -356,13 +488,11 @@ void main() { ## 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=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -379,7 +509,7 @@ void main() { 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 = ''' @@ -388,14 +518,14 @@ void main() { ## $version * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=master']); - await expectLater( + runner, ['version-check', '--base-sha=main']); + expect( output, containsAllInOrder([ contains('Running for plugin'), @@ -406,7 +536,7 @@ void main() { 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 = ''' @@ -417,13 +547,11 @@ void main() { ## 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=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -433,7 +561,7 @@ void main() { 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.") ]), ); }); @@ -441,7 +569,7 @@ void main() { 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 = ''' @@ -452,17 +580,12 @@ void main() { ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -472,15 +595,15 @@ void main() { 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 = ''' @@ -489,17 +612,12 @@ void main() { ## 1.0.0 * Some other changes. '''; - createFakeCHANGELOG(pluginDirectory, changelog); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + plugin.changelogFile.writeAsStringSync(changelog); bool hasError = false; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -509,11 +627,459 @@ void main() { 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.'), ]), ); }); + test( + 'fails gracefully if the version headers are not found due to using the wrong style', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## NEXT +* Some changes for a later release. +# 1.0.0 +* Some other changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=main', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to find a version in CHANGELOG.md'), + contains('The current version should be on a line starting with ' + '"## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'), + ]), + ); + }); + + test('fails gracefully if the version is unparseable', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## Alpha +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=main', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"Alpha" could not be parsed as a version.'), + ]), + ); + }); + + group('missing change detection', () { + Future> _runWithMissingChangeDetection( + List extraArgs, + {void Function(Error error)? errorHandler}) async { + return runCapturingPrint( + runner, + [ + 'version-check', + '--base-sha=main', + '--check-for-missing-changes', + ...extraArgs, + ], + errorHandler: errorHandler); + } + + test('passes for unchanged packages', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test( + 'fails if a version change is missing from a change that does not ' + 'pass the exemption check', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/lib/plugin.dart +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + [], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No version change found'), + contains('plugin:\n' + ' Missing version change'), + ]), + ); + }); + + test('passes version change requirement when version changes', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.1'); + + const String changelog = ''' +## 1.0.1 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/lib/plugin.dart +packages/plugin/CHANGELOG.md +packages/plugin/pubspec.yaml +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('version change check ignores files outside the package', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin_a/lib/plugin.dart +tool/plugin/lib/plugin.dart +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('allows missing version change for exempt changes', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/android/lint-baseline.xml +packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java +packages/plugin/example/ios/RunnerTests/Foo.m +packages/plugin/example/ios/RunnerUITests/info.plist +packages/plugin/CHANGELOG.md +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('allows missing version change with justification', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/lib/plugin.dart +packages/plugin/CHANGELOG.md +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}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of version change due to ' + '"No version change:" in the change description.'), + ]), + ); + }); + + test('fails if a CHANGELOG change is missing', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + [], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No CHANGELOG change found'), + contains('plugin:\n' + ' Missing CHANGELOG change'), + ]), + ); + }); + + test('passes CHANGELOG check when the CHANGELOG is changed', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +packages/plugin/CHANGELOG.md +'''), + ]; + + final List output = + await _runWithMissingChangeDetection([]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('fails CHANGELOG check if only another package CHANGELOG chages', + () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +packages/another_plugin/CHANGELOG.md +'''), + ]; + + Error? commandError; + final List output = await _runWithMissingChangeDetection( + [], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No CHANGELOG change found'), + ]), + ); + }); + + test('allows missing CHANGELOG change with justification', () async { + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## 1.0.0 +* Some changes. +'''; + plugin.changelogFile.writeAsStringSync(changelog); + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin/example/lib/foo.dart +'''), + ]; + + final 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}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Ignoring lack of CHANGELOG update due to ' + '"No CHANGELOG change:" in the change description.'), + ]), + ); + }); + }); + test('allows valid against pub', () async { mockHttpResponse = { 'name': 'some_package', @@ -525,11 +1091,8 @@ void main() { }; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; final List output = await runCapturingPrint(runner, - ['version-check', '--base-sha=master', '--against-pub']); + ['version-check', '--base-sha=main', '--against-pub']); expect( output, @@ -549,16 +1112,11 @@ void main() { }; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; bool hasError = false; - final List result = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List result = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -581,15 +1139,10 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N mockHttpStatus = 400; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; bool hasError = false; - final List result = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=master', - '--against-pub' - ], errorHandler: (Error e) { + final List result = await runCapturingPrint( + runner, ['version-check', '--base-sha=main', '--against-pub'], + errorHandler: (Error e) { expect(e, isA()); hasError = true; }); @@ -612,11 +1165,11 @@ ${indentation}HTTP response: null mockHttpStatus = 404; createFakePlugin('plugin', packagesDir, version: '2.0.0'); - gitShowResponses = { - 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', - }; + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess(stdout: 'version: 1.0.0'), + ]; final List result = await runCapturingPrint(runner, - ['version-check', '--base-sha=master', '--against-pub']); + ['version-check', '--base-sha=main', '--against-pub']); expect( result, @@ -625,6 +1178,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 10008ae33a11..418c695f295c 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -58,7 +58,7 @@ void main() { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); final List output = @@ -71,7 +71,7 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformIos: const PlatformDetails(PlatformSupport.federated) + platformIOS: const PlatformDetails(PlatformSupport.federated) }); final List output = @@ -82,13 +82,12 @@ void main() { }); test('runs for iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: 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,10 +123,51 @@ 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: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline) + platformIOS: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -173,7 +213,7 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, platformSupport: { - kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + platformMacOS: const PlatformDetails(PlatformSupport.federated), }); final List output = await runCapturingPrint( @@ -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: { - kPlatformMacos: 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', @@ -221,10 +259,45 @@ 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: { - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + platformMacOS: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -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: { - kPlatformIos: const PlatformDetails(PlatformSupport.inline), - kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + 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: { - kPlatformMacos: 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', @@ -353,13 +422,12 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: 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 99bab387e6b6..66181543f2cf 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -19,4 +19,4 @@ 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 $PLUGIN_SHARDING +dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PLUGIN_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